Files
RouterOS/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift
T
KayandClaude Sonnet 5 0237b3afbe Dauer-Editor auf Keepalive/Scheduler-Intervall/Netwatch-Intervall ausgerollt
WireGuard-Peer "Keepalive", Scheduler "Intervall" und Netwatch
"Prüf-Intervall" nutzen jetzt RouterOSFieldSchema.Kind.duration statt
Freitext. Netwatch-Default von "00:00:10" auf "10s" umgestellt, da
DurationFieldEditor.parse nur das Suffix-Format versteht, kein
HH:MM:SS. Vorher live geprüft, dass RouterOS das Suffix-Format bei
allen drei Feldern akzeptiert. Live bestätigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 10:03:35 +02:00

786 lines
59 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Curated catalog of RouterOS menus for the Expert tool. Depth is intentionally uneven: the
/// Firewall/NAT/Mangle/Address-List family (what was explicitly asked for) has full field
/// schemas with tooltips and warnings; everything else has enough to browse/add/remove items
/// generically (empty `fields`, so the Expert tool falls back to a raw key=value form for those)
/// while still being properly labeled and explained. RouterOS has 300+ menus in total — this
/// catalog is a curated shortlist of the ones that matter for a home/small-office router, not
/// an exhaustive mirror of the OS. Anything not listed here is still reachable in the Expert
/// tool via its "eigener Menüpfad" free-entry field, generically.
enum RouterOSSchemaCatalog {
static let all: [RouterOSMenuSchema] = firewallFamily + interfaceFamily + ipFamily
+ routingFamily + vpnFamily + wirelessFamily + queueFamily + systemFamily + toolFamily
static func schemas(in category: RouterOSMenuCategory) -> [RouterOSMenuSchema] {
all.filter { $0.category == category }
}
// MARK: - Firewall (fully curated)
private static let firewallFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ip firewall filter",
restPath: "ip/firewall/filter",
category: .firewallFilter,
displayName: "Filter-Regeln",
summary: "Entscheidet, ob Pakete durchgelassen (accept), verworfen (drop/reject) oder weiter geprüft werden.",
explanation: "input = Zugriffe auf den Router selbst, forward = Verkehr, der durch den Router hindurchgeht (z.B. LAN↔Internet oder zwischen zwei Netzen), output = vom Router ausgehender Verkehr. Regeln werden von oben nach unten geprüft; die erste passende Regel entscheidet.",
warning: "Reihenfolge zählt. Neue Regeln landen am Ende der Liste — eine bereits vorhandene, weiter oben stehende Regel kann eine neue Regel unerreichbar machen. Position danach mit \"/ip firewall filter print\" kontrollieren, notfalls mit \"move\" verschieben.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .text,
help: "input = an den Router, forward = durch den Router, output = vom Router aus. Eigene Chains (Sprungziele über \"jump\") sind ebenfalls möglich. Beispiel: forward",
defaultValue: "forward", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"accept", "drop", "reject", "log", "jump", "return",
"add-src-to-address-list", "add-dst-to-address-list", "fasttrack-connection", "passthrough"
]), help: "Was mit passenden Paketen geschieht. reject schickt zusätzlich eine Fehlermeldung zurück, drop verwirft stillschweigend.",
defaultValue: "accept", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.5 oder 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.5 oder 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "src-address-list", label: "Quelle in Adress-Liste",
kind: .menuItemPick(menuPath: "/ip firewall address-list", restPath: "ip/firewall/address-list", valueField: "list"),
help: "Nur Pakete, deren Absender in dieser zuvor angelegten Adress-Liste steht."),
RouterOSFieldSchema(key: "dst-address-list", label: "Ziel in Adress-Liste",
kind: .menuItemPick(menuPath: "/ip firewall address-list", restPath: "ip/firewall/address-list", valueField: "list"),
help: "Nur Pakete, deren Ziel in dieser zuvor angelegten Adress-Liste steht."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hereinkommen, z.B. ether1 oder bridge."),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hinausgehen, z.B. ether1 oder bridge."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: [
"tcp", "udp", "icmp", "gre", "ipsec-esp", "ipsec-ah"
]), help: "IP-Protokoll. Leer = alle."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text,
help: "Nur bei tcp/udp sinnvoll. Einzelner Port (z.B. 80) oder Bereich (z.B. 8000-8100)."),
RouterOSFieldSchema(key: "connection-state", label: "Verbindungsstatus", kind: .enumPick(options: [
"new", "established", "related", "invalid", "untracked"
]), help: "Status laut Connection-Tracking. \"established,related\" ist die übliche \"schon erlaubte Verbindung\"-Regel."),
RouterOSFieldSchema(key: "layer7-protocol", label: "Layer7-Protokoll", kind: .text,
help: "Name eines vorher unter \"/ip firewall layer7-protocol\" angelegten Musters (z.B. Erkennung bestimmter Apps). Kostet spürbar CPU, mit Bedacht einsetzen."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text,
help: "Nur zur eigenen Wiedererkennung, ohne technische Wirkung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall nat",
restPath: "ip/firewall/nat",
category: .firewallNat,
displayName: "NAT-Regeln",
summary: "Übersetzt Adressen — Internetfreigabe (srcnat/masquerade) und Portweiterleitung (dstnat).",
explanation: "srcnat ändert die Absenderadresse ausgehender Pakete (z.B. private LAN-IP → öffentliche WAN-IP). dstnat ändert die Zieladresse eingehender Pakete (z.B. Anfrage an die WAN-IP auf Port 80 → internen Server auf 192.168.88.10:80 umleiten).",
warning: "Eine dstnat-Regel (Portweiterleitung) macht ein internes Gerät direkt aus dem Internet erreichbar — nur für Dienste einrichten, die das wirklich sein sollen, und danach den entsprechenden Port in der Filter-Tabelle nicht blockieren.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: ["srcnat", "dstnat"]),
help: "srcnat = Absenderadresse ändern (Internetfreigabe). dstnat = Zieladresse ändern (Portweiterleitung).",
defaultValue: "srcnat", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"masquerade", "src-nat", "dst-nat", "netmap", "redirect", "same"
]), help: "masquerade = automatisches NAT über die aktuelle WAN-IP (empfohlen bei wechselnder IP). src-nat = feste NAT-Adresse. dst-nat = Ziel umschreiben (Portweiterleitung). redirect = auf den Router selbst umlenken.",
defaultValue: "masquerade", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Bei dstnat meist der WAN-Port (woher die Anfrage aus dem Internet kommt), z.B. ether1."),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick,
help: "Bei srcnat/masquerade meist der WAN-Port, z.B. ether1."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]),
help: "Nötig, damit Ports geprüft werden können."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port der Anfrage", kind: .text,
help: "Der Port, auf den die Anfrage von außen ankommt, z.B. 8080."),
RouterOSFieldSchema(key: "to-addresses", label: "Weiterleiten an (interne IP)", kind: .text,
help: "Nur bei dst-nat: die interne IP-Adresse, an die weitergeleitet wird, z.B. 192.168.88.10."),
RouterOSFieldSchema(key: "to-ports", label: "Weiterleiten an (interner Port)", kind: .text,
help: "Nur bei dst-nat: interner Port, falls abweichend vom Ziel-Port (z.B. extern 8080 → intern 80)."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall mangle",
restPath: "ip/firewall/mangle",
category: .firewallMangle,
displayName: "Mangle-Regeln",
summary: "Markiert Verbindungen/Pakete zur späteren Weiterverarbeitung (z.B. durch Queues).",
explanation: "Mangle selbst verändert nicht, wie ein Paket behandelt wird — es klebt nur eine Markierung darauf. Erst eine andere Regel (typischerweise eine Queue oder eine Routing-Regel), die genau diese Markierung als Bedingung abfragt, wird dadurch wirksam.",
warning: "Eine Markierung ohne etwas, das sie auswertet (z.B. keine passende Queue), hat keinerlei sichtbaren Effekt — das ist der häufigste Verwirrungspunkt bei Mangle.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: [
"prerouting", "input", "forward", "output", "postrouting"
]), help: "Verarbeitungspunkt im Router-internen Paketfluss.", defaultValue: "forward", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"mark-connection", "mark-packet", "mark-routing", "change-mss", "change-ttl", "set-priority", "accept", "passthrough"
]), help: "mark-connection markiert die ganze Verbindung (wirkt auf alle ihre Pakete), mark-packet nur einzelne Pakete.",
defaultValue: "mark-connection", required: true),
RouterOSFieldSchema(key: "new-connection-mark", label: "Name der Verbindungsmarkierung", kind: .text,
help: "Frei wählbarer Name, den z.B. eine Queue später wiedererkennt."),
RouterOSFieldSchema(key: "new-packet-mark", label: "Name der Paketmarkierung", kind: .text,
help: "Frei wählbarer Name für die Paketmarkierung."),
RouterOSFieldSchema(key: "passthrough", label: "Weitere Mangle-Regeln noch prüfen", kind: .bool,
help: "Ja (Standard) lässt nachfolgende Mangle-Regeln diese Verbindung noch zusätzlich prüfen.", defaultValue: "yes"),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hereinkommen, z.B. ether1 oder bridge."),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hinausgehen, z.B. ether1 oder bridge."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]),
help: "IP-Protokoll. Leer = alle."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text,
help: "Nur bei tcp/udp sinnvoll. Einzelner Port (z.B. 80) oder Bereich (z.B. 8000-8100)."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall raw",
restPath: "ip/firewall/raw",
category: .firewallRaw,
displayName: "Raw-Regeln",
summary: "Läuft vor jeder Connection-Tracking-Verarbeitung — meist zur Entlastung oder für DDoS-Grobfilter.",
explanation: "Regeln hier greifen, bevor RouterOS eine Verbindung überhaupt \"kennt\" (Connection-Tracking). Damit lässt sich z.B. bekannt uninteressanter Verkehr sehr günstig verwerfen, oder gezielt vom Tracking ausnehmen (notrack).",
warning: "notrack nimmt den betroffenen Verkehr aus dem Connection-Tracking heraus — danach greifen dafür keine Filter-/NAT-Regeln mehr, die sich auf connection-state oder NAT verlassen. Nur gezielt einsetzen.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: ["prerouting", "output"]),
help: "prerouting = eingehende Pakete, bevor der Router sie verarbeitet. output = vom Router selbst erzeugte Pakete.",
defaultValue: "prerouting", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: ["accept", "drop", "notrack"]),
help: "notrack = von der Connection-Tracking-Verfolgung ausnehmen (siehe Warnhinweis oben).",
defaultValue: "accept", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.0/24. Leer = beliebig."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hereinkommen, z.B. ether1."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]),
help: "IP-Protokoll. Leer = alle."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text,
help: "Nur bei tcp/udp sinnvoll. Einzelner Port (z.B. 80) oder Bereich (z.B. 8000-8100)."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur eigenen Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall address-list",
restPath: "ip/firewall/address-list",
category: .firewallAddressLists,
displayName: "Adress-Listen",
summary: "Benannte Gruppen von IP-Adressen/Netzen, die Filter/NAT/Mangle-Regeln als Bedingung referenzieren können.",
explanation: "Statt in jeder Regel einzelne IPs aufzuzählen, legst du hier eine benannte Liste an (z.B. \"gesperrt\" oder \"vertraut\") und verweist in Filter-/NAT-/Mangle-Regeln per \"src-address-list\"/\"dst-address-list\" darauf. Einträge können dauerhaft oder mit Ablaufzeit (timeout) sein.",
fields: [
RouterOSFieldSchema(key: "list", label: "Listen-Name", kind: .text,
help: "Frei wählbarer Name, wird in anderen Regeln referenziert, z.B. gesperrt.", required: true),
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text,
help: "Einzel-IP oder Netz mit Präfix, z.B. 192.168.88.5 oder 10.0.0.0/24.", required: true),
RouterOSFieldSchema(key: "timeout", label: "Ablaufzeit", kind: .duration,
help: "Optional. Nach dieser Zeit wird der Eintrag automatisch entfernt. Alles auf 0 lassen = bleibt dauerhaft, bis manuell entfernt."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur eigenen Wiedererkennung.")
],
listColumns: ["list", "address"]
)
]
// MARK: - Interfaces (generic, browsing + free-form fields)
private static let interfaceFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/interface", restPath: "interface", category: .interfaces,
displayName: "Alle Interfaces (generisch)",
summary: "Gemeinsame Felder, die für jedes Interface gelten, unabhängig vom Typ (Ethernet, Bridge, WLAN, WireGuard, VLAN, ...).",
explanation: "RouterOS listet hier alle Interfaces zusammen. Typ-spezifische Felder (z.B. die VLAN-ID eines VLAN-Interfaces) liegen in den jeweils eigenen Menüs (z.B. \"VLAN-Interfaces\") — hier nur das, was für jeden Interface-Typ gleich funktioniert.",
warning: "RouterOS liefert hier zusätzlich einen \"default-name\" (Werksname des Ports) mit — der erscheint unten bei \"Weitere Parameter\", ist aber nicht änderbar (\"bad parameter default-name\", live bestätigt). Zum Umbenennen nur das Feld \"Name\" oben verwenden.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text,
help: "Der aktuelle Name dieses Interfaces, z.B. ether5 oder vlan20 — nicht zu verwechseln mit \"default-name\" (Werksname, weiter unten bei \"Weitere Parameter\", nicht änderbar).", required: true),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text,
help: "Nur zur eigenen Wiedererkennung, ohne technische Wirkung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["name"]
),
RouterOSMenuSchema(
menuPath: "/interface bridge", restPath: "interface/bridge", category: .interfaces,
displayName: "Bridge",
summary: "Fasst mehrere physische Ports zu einem gemeinsamen Layer-2-Netzwerk zusammen.",
explanation: "Geräte an gebrückten Ports verhalten sich, als hingen sie am selben Netzwerk-Kabel. Eine IP-Adresse wird meist auf die Bridge selbst gelegt, nicht auf die einzelnen Ports.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. bridge-lan.", required: true),
RouterOSFieldSchema(key: "vlan-filtering", label: "VLAN-Filterung (802.1Q)", kind: .bool,
help: "Aktiviert echte VLAN-Trennung über diese Bridge — nötig, wenn mehrere VLANs über dieselben Bridge-Ports laufen sollen.", defaultValue: "no"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Bridge inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["name"]
),
RouterOSMenuSchema(
menuPath: "/interface bridge port", restPath: "interface/bridge/port", category: .interfaces,
displayName: "Bridge-Ports",
summary: "Ordnet einen physischen Port einer Bridge zu.",
explanation: "Erst danach ist der Port Teil des Bridge-Netzwerks.",
fields: [
RouterOSFieldSchema(key: "bridge", label: "Bridge", kind: .text, help: "Name der Bridge, z.B. bridge-lan.", required: true),
RouterOSFieldSchema(key: "interface", label: "Physischer Port", kind: .interfacePick,
help: "Der Port, der der Bridge hinzugefügt wird, z.B. ether2.", required: true),
RouterOSFieldSchema(key: "pvid", label: "Port-VLAN-ID (PVID)", kind: .int,
help: "Nur mit aktivierter VLAN-Filterung relevant: VLAN, dem untagged ankommender Verkehr an diesem Port zugeordnet wird, z.B. 20.")
],
listColumns: ["bridge", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface vlan", restPath: "interface/vlan", category: .interfaces,
displayName: "VLAN-Interfaces",
summary: "Virtuelle, getaggte Sub-Interfaces auf einem physischen Port oder einer Bridge.",
explanation: "Ein eigenes logisches Netzwerk auf demselben Kabel, unterschieden durch eine VLAN-Kennung im Ethernet-Rahmen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. vlan20-gaeste.", required: true),
RouterOSFieldSchema(key: "vlan-id", label: "VLAN-ID", kind: .int, help: "Eindeutige Kennung, 24094, z.B. 20.", required: true),
RouterOSFieldSchema(key: "interface", label: "Basis-Interface", kind: .interfacePick,
help: "Physischer Port oder Bridge, auf dem dieses VLAN aufsetzt, z.B. bridge.", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "VLAN-Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["name", "vlan-id", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface wireguard", restPath: "interface/wireguard", category: .interfaces,
displayName: "WireGuard-Interfaces",
summary: "Moderner, schlanker VPN-Tunnel-Typ.",
explanation: "Ein WireGuard-Interface allein stellt noch keine Verbindung her — dazu gehören Gegenstellen (siehe \"WireGuard-Peers\") mit öffentlichem Schlüssel und erlaubten Adressen.",
warning: "Erst hier das Interface anlegen (mit Listen-Port), danach unter WireGuard-Peers die Gegenstellen eintragen. Der private Schlüssel wird beim Anlegen automatisch erzeugt, falls nicht angegeben.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. wg-heim.", required: true),
RouterOSFieldSchema(key: "listen-port", label: "Listen-Port (UDP)", kind: .int,
help: "Port, auf dem dieser Tunnel auf eingehende Verbindungen lauscht, z.B. 51820.", defaultValue: "51820"),
RouterOSFieldSchema(key: "private-key", label: "Privater Schlüssel", kind: .text,
help: "Geheim halten. Leer lassen, damit RouterOS automatisch einen erzeugt."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["name", "listen-port"]
),
RouterOSMenuSchema(
menuPath: "/interface wireguard peers", restPath: "interface/wireguard/peers", category: .interfaces,
displayName: "WireGuard-Peers",
summary: "Gegenstellen (Clients/andere Router) eines WireGuard-Tunnels.",
explanation: "Jede Gegenstelle braucht ihren eigenen öffentlichen Schlüssel und eine Angabe, welche Adressen über sie geroutet werden.",
fields: [
RouterOSFieldSchema(key: "interface", label: "WireGuard-Interface",
kind: .menuItemPick(menuPath: "/interface wireguard", restPath: "interface/wireguard", valueField: "name"),
help: "Name des zuvor angelegten WireGuard-Interfaces.", required: true),
RouterOSFieldSchema(key: "public-key", label: "Öffentlicher Schlüssel der Gegenstelle", kind: .text,
help: "Vom Gerät der Gegenstelle kopieren (z.B. per \"wg show <interface> public-key\").", required: true),
RouterOSFieldSchema(key: "allowed-address", label: "Erlaubte Adressen", kind: .text,
help: "Welche IP-Adressen/Netze über diesen Peer laufen dürfen, mit Präfix, z.B. 10.10.10.2/32."),
RouterOSFieldSchema(key: "endpoint-address", label: "Feste Adresse der Gegenstelle", kind: .text,
help: "Nur nötig, wenn diese Gegenstelle selbst erreichbar sein muss (Site-to-Site), z.B. eine feste öffentliche IP oder ein DNS-Name. Bei Roadwarrior-Clients, die sich selbst melden, leer lassen."),
RouterOSFieldSchema(key: "endpoint-port", label: "Port der Gegenstelle", kind: .text,
help: "Meist derselbe Port wie der Listen-Port der Gegenstelle, z.B. 51820."),
RouterOSFieldSchema(key: "persistent-keepalive", label: "Keepalive", kind: .duration,
help: "Hält die Verbindung durch NAT/Firewalls am Leben. Wichtig bei Clients hinter NAT.")
],
listColumns: ["interface", "allowed-address"]
),
RouterOSMenuSchema(
menuPath: "/interface pppoe-client", restPath: "interface/pppoe-client", category: .interfaces,
displayName: "PPPoE-Client",
summary: "DSL-Einwahl-Client, meist auf dem WAN-Port.",
explanation: "Ersetzt eine feste/DHCP-WAN-Adresse durch eine PPPoE-Einwahl beim Provider.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. pppoe-wan.", required: true),
RouterOSFieldSchema(key: "interface", label: "Physischer Port", kind: .interfacePick,
help: "Der Port, über den die Einwahl läuft, meist der WAN-Port, z.B. ether1.", required: true),
RouterOSFieldSchema(key: "user", label: "Benutzername", kind: .text, help: "Zugangsdaten des Providers.", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "Zugangsdaten des Providers.", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Einwahl inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["name", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface bonding", restPath: "interface/bonding", category: .interfaces,
displayName: "Bonding",
summary: "Bündelt mehrere physische Ports zu einer logischen, ausfalltoleranten/schnelleren Verbindung.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. bond1.", required: true),
RouterOSFieldSchema(key: "slaves", label: "Gebündelte Ports", kind: .text,
help: "Kommagetrennte Liste physischer Ports, z.B. ether2,ether3."),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: [
"802.3ad", "active-backup", "balance-rr", "balance-xor", "broadcast"
]), help: "802.3ad (LACP) braucht einen kompatiblen, entsprechend konfigurierten Switch.")
],
listColumns: ["name", "mode"]
)
]
// MARK: - IP addressing & services
private static let ipFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ip address", restPath: "ip/address", category: .ipAddressing,
displayName: "IP-Adressen",
summary: "Weist Interfaces IP-Adressen zu.",
explanation: "Jede IP-Adresse hängt an genau einem Interface (physischer Port, Bridge oder VLAN).",
fields: [
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text,
help: "IP-Adresse mit Netzmaske als Präfix (Router-Adresse in diesem Netz), z.B. 192.168.88.1/24 oder 10.10.10.1/24. Das /24 legt fest, wie viele Geräte in dieses Netz passen (/24 = bis zu 254).", required: true),
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick,
help: "Interface, dem diese Adresse zugewiesen wird, z.B. bridge oder ether4.", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Adresse inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["address", "interface"]
),
RouterOSMenuSchema(
menuPath: "/ip pool", restPath: "ip/pool", category: .ipAddressing,
displayName: "Adress-Pools",
summary: "Adressbereiche, aus denen DHCP-Server oder PPP-Profile Adressen vergeben.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. dhcp_pool_lan.", required: true),
RouterOSFieldSchema(key: "ranges", label: "Bereich(e)", kind: .text,
help: "Von-bis-Adresse ohne Präfix, z.B. 192.168.88.10-192.168.88.254. Mehrere Bereiche kommagetrennt.", required: true)
],
listColumns: ["name", "ranges"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-server", restPath: "ip/dhcp-server", category: .ipAddressing,
displayName: "DHCP-Server",
summary: "Vergibt automatisch IP-Adressen an Geräte in einem Netzwerk.",
explanation: "Für die üblichen Fälle deckt das bereits der Einrichten-Assistent (LAN-/VLAN-Schritt) ab — hier direkter Zugriff für Sonderfälle.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. dhcp_lan.", required: true),
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick,
help: "Netzwerk, in dem dieser Server Adressen vergibt, z.B. bridge.", required: true),
RouterOSFieldSchema(key: "address-pool", label: "Adress-Pool",
kind: .menuItemPick(menuPath: "/ip pool", restPath: "ip/pool", valueField: "name"),
help: "Name eines zuvor angelegten Adress-Pools.", required: true),
RouterOSFieldSchema(key: "lease-time", label: "Lease-Zeit", kind: .duration,
help: "Wie lange ein Gerät seine Adresse behält, bevor sie erneuert werden muss, z.B. 1d oder 12h."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "DHCP-Server inaktiv schalten, ohne ihn zu löschen.", defaultValue: "no")
],
listColumns: ["name", "interface"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-server network", restPath: "ip/dhcp-server/network", category: .ipAddressing,
displayName: "DHCP-Netzwerke",
summary: "Gateway/DNS/Optionen, die ein DHCP-Server an seine Klienten verteilt.",
explanation: "Getrennt vom DHCP-Server selbst, weil dieselben Netzwerk-Optionen für mehrere DHCP-Server gelten können.",
fields: [
RouterOSFieldSchema(key: "address", label: "Netz", kind: .text,
help: "Für dieses Netz gelten die folgenden Optionen — die Netzadresse mit Präfix, z.B. 192.168.88.0/24.", required: true),
RouterOSFieldSchema(key: "gateway", label: "Gateway", kind: .text,
help: "In der Regel die Router-Adresse in diesem Netz, ohne Präfix, z.B. 192.168.88.1.", required: true),
RouterOSFieldSchema(key: "dns-server", label: "DNS-Server", kind: .text,
help: "Meist der Router selbst, z.B. 192.168.88.1. Mehrere Server kommagetrennt möglich.")
],
listColumns: ["address", "gateway"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-client", restPath: "ip/dhcp-client", category: .ipAddressing,
displayName: "DHCP-Client (WAN)",
summary: "Bezieht automatisch eine IP-Adresse vom Internetanbieter.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick,
help: "Meist der WAN-Port, z.B. ether1.", required: true),
RouterOSFieldSchema(key: "add-default-route", label: "Standardroute übernehmen", kind: .bool,
help: "Übernimmt die vom Provider mitgeteilte Standardroute ins Internet.", defaultValue: "yes"),
RouterOSFieldSchema(key: "use-peer-dns", label: "DNS-Server übernehmen", kind: .bool,
help: "Übernimmt die vom Provider mitgeteilten DNS-Server.", defaultValue: "yes"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "DHCP-Client inaktiv schalten, ohne ihn zu löschen.", defaultValue: "no")
],
listColumns: ["interface"]
),
RouterOSMenuSchema(
menuPath: "/ip dns", restPath: "ip/dns", category: .ipAddressing,
displayName: "DNS-Einstellungen",
summary: "Namensauflösung des Routers selbst (und optional als DNS-Server fürs LAN).",
explanation: "",
fields: [
RouterOSFieldSchema(key: "servers", label: "DNS-Server", kind: .text,
help: "Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8."),
RouterOSFieldSchema(key: "allow-remote-requests", label: "Als DNS-Server fürs LAN erlauben", kind: .bool,
help: "Lässt Geräte im LAN den Router selbst als DNS-Server nutzen. Ohne das schlagen DNS-Anfragen von Geräten an den Router fehl, selbst wenn sie ihn als DNS-Server eingetragen haben.", defaultValue: "no")
],
listColumns: ["servers"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/ip service", restPath: "ip/service", category: .ipAddressing,
displayName: "Verwaltungsdienste",
summary: "Schaltet Zugriffswege auf den Router (Winbox, API, SSH, WWW/REST, Telnet, FTP) an/aus und ändert deren Port.",
explanation: "Jeder aktive Dienst ist ein potenzieller Angriffspunkt aus dem jeweils erreichbaren Netz — nicht benötigte Dienste deaktivieren.",
warning: "Den gerade genutzten Zugriffsweg hier abzuschalten oder umzuportieren kann die eigene Verbindung sofort kappen — Vorsicht bei www-ssl (REST-API dieser App) und ssh.",
fields: [
RouterOSFieldSchema(key: "name", label: "Dienst", kind: .enumPick(options: [
"www", "www-ssl", "ssh", "api", "api-ssl", "winbox", "ftp", "telnet"
]), help: "Welcher Verwaltungsdienst geändert wird.", required: true),
RouterOSFieldSchema(key: "port", label: "Port", kind: .int, help: "Auf welchem Port der Dienst lauscht, z.B. 22 für ssh."),
RouterOSFieldSchema(key: "available-from", label: "Erlaubt von", kind: .text,
help: "Optional: nur von dieser Adresse/diesem Netz aus erreichbar, mit Präfix, z.B. 192.168.88.0/24."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Dienst inaktiv schalten, ohne den Eintrag zu löschen.", defaultValue: "no")
],
listColumns: ["name", "port"]
),
generic("/ip hotspot", "ip/hotspot", .ipAddressing, "Hotspot",
"Login-Portal für Gäste-WLAN/-LAN mit Umleitung auf eine Anmeldeseite.",
"Besteht aus mehreren zusammenhängenden Teilen (Server, Server-Profil, Benutzer-Profil, Benutzer) — hier generischer Zugriff auf \"/ip hotspot\" selbst, verwandte Teile über \"Eigener Menüpfad\" (z.B. \"/ip hotspot user\")."),
RouterOSMenuSchema(
menuPath: "/ip route", restPath: "ip/route", category: .routing,
displayName: "Statische Routen",
summary: "Feste, manuell eingetragene Wegewahl zu Netzen, die nicht direkt angeschlossen sind.",
explanation: "Für alles außer \"Standard-Internet über eine WAN-Schnittstelle\" (das übernimmt bereits die DHCP-Client-/PPPoE-Route automatisch).",
fields: [
RouterOSFieldSchema(key: "dst-address", label: "Zielnetz", kind: .text,
help: "Netz mit Präfix, z.B. 10.0.0.0/24, oder 0.0.0.0/0 für eine Standardroute.", required: true),
RouterOSFieldSchema(key: "gateway", label: "Gateway", kind: .text,
help: "Nächster Hop — IP-Adresse ohne Präfix (z.B. 192.168.88.254) oder Name eines Interfaces (z.B. ether1).", required: true),
RouterOSFieldSchema(key: "distance", label: "Distanz", kind: .int,
help: "Priorität bei mehreren passenden Routen zum selben Ziel — kleinere Zahl wird bevorzugt, z.B. 1."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur eigenen Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Route inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["dst-address", "gateway"]
)
]
// MARK: - Routing (dynamic protocols)
private static let routingFamily: [RouterOSMenuSchema] = [
generic("/routing ospf instance", "routing/ospf/instance", .routing, "OSPF-Instanzen",
"Dynamisches Innennetz-Routing-Protokoll — tauscht Routen automatisch mit anderen OSPF-Routern aus.",
"Nur relevant, wenn mehrere Router im selben Netz eigenständig Routen lernen sollen.",
warning: "Fehlkonfiguriertes OSPF kann Routen zu bestehenden Netzen überschreiben. Nur mit Netzwerkplan einsetzen."),
generic("/routing bgp connection", "routing/bgp/connection", .routing, "BGP-Verbindungen",
"Routing-Protokoll für Verbindungen zwischen unabhängigen Netzen/Providern.",
"Für Privat-/Kleinnetz i.d.R. nicht nötig — relevant bei eigenem Provider-unabhängigem Adressraum (Multihoming).")
]
// MARK: - VPN
private static let vpnFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ppp secret", restPath: "ppp/secret", category: .vpn,
displayName: "PPP-Benutzer",
summary: "Zugangsdaten für PPPoE-/L2TP-/PPTP-/OpenVPN-Einwahl in den Router.",
explanation: "Jeder Benutzer kann optional einem Profil zugeordnet werden, das IP-Pool/DNS/Verschlüsselung vorgibt.",
fields: [
RouterOSFieldSchema(key: "name", label: "Benutzername", kind: .text, help: "Frei wählbar, z.B. gast1.", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "Ausreichend lang und zufällig wählen.", required: true),
RouterOSFieldSchema(key: "service", label: "Dienst", kind: .enumPick(options: [
"any", "pppoe", "l2tp", "pptp", "ovpn", "sstp"
]), help: "Für welche Einwahl-Art dieser Benutzer gilt.", defaultValue: "any"),
RouterOSFieldSchema(key: "profile", label: "Profil",
kind: .menuItemPick(menuPath: "/ppp profile", restPath: "ppp/profile", valueField: "name"),
help: "Name eines zuvor angelegten PPP-Profils."),
RouterOSFieldSchema(key: "local-address", label: "Lokale Adresse", kind: .text,
help: "IP-Adresse des Routers innerhalb dieser Verbindung, ohne Präfix, z.B. 10.10.10.1."),
RouterOSFieldSchema(key: "remote-address", label: "Adresse für den Client", kind: .text,
help: "Feste IP für diesen Benutzer (z.B. 10.10.10.2), oder Name eines Adress-Pools."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Benutzer inaktiv schalten, ohne ihn zu löschen.", defaultValue: "no")
],
listColumns: ["name", "service"]
),
RouterOSMenuSchema(
menuPath: "/ppp profile", restPath: "ppp/profile", category: .vpn,
displayName: "PPP-Profile",
summary: "Vorlagen (IP-Pool, DNS, Verschlüsselung) für PPP-Benutzer.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. vpn-clients.", required: true),
RouterOSFieldSchema(key: "local-address", label: "Lokale Adresse", kind: .text,
help: "IP-Adresse des Routers, ohne Präfix, z.B. 10.10.10.1."),
RouterOSFieldSchema(key: "remote-address", label: "Adress-Pool für Clients", kind: .text,
help: "Name eines zuvor angelegten Adress-Pools."),
RouterOSFieldSchema(key: "dns-server", label: "DNS-Server für Clients", kind: .text,
help: "Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8.")
],
listColumns: ["name"]
),
generic("/interface l2tp-server server", "interface/l2tp-server/server", .vpn, "L2TP-VPN-Server",
"Nimmt eingehende L2TP-VPN-Einwahlen entgegen.",
"Ein einzelnes Server-weites An/Aus mit gemeinsamen Verschlüsselungs-Einstellungen — Benutzer selbst kommen von \"PPP-Benutzer\"."),
generic("/interface ovpn-server server", "interface/ovpn-server/server", .vpn, "OpenVPN-Server",
"Nimmt eingehende OpenVPN-Einwahlen entgegen.",
"Braucht zusätzlich ein Zertifikat (\"/certificate\") — Benutzer selbst kommen von \"PPP-Benutzer\".")
]
// MARK: - Wireless
private static let wirelessFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/interface wireless", restPath: "interface/wireless", category: .wireless,
displayName: "WLAN (Legacy-Treiber)",
summary: "WLAN-Interfaces auf älteren/Standard-Wireless-Chips.",
explanation: "Diese Interfaces existieren bereits ab Werk (ein WLAN-Chip = ein Interface) — hier werden sie nur konfiguriert, nicht neu angelegt. Ein Sicherheitsprofil (siehe \"WLAN-Sicherheitsprofile\") muss vorher angelegt sein.",
fields: [
RouterOSFieldSchema(key: "ssid", label: "Netzwerkname (SSID)", kind: .text,
help: "Der Name, den WLAN-Geräte in der Netzwerkliste sehen, z.B. MeinWLAN.", required: true),
RouterOSFieldSchema(key: "security-profile", label: "Sicherheitsprofil", kind: .text,
help: "Name eines zuvor unter \"WLAN-Sicherheitsprofile\" angelegten Profils."),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: ["ap-bridge", "station", "bridge"]),
help: "ap-bridge = Access Point (Normalfall), station = als Client mit einem anderen AP verbinden.", defaultValue: "ap-bridge"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "WLAN-Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["ssid", "mode"]
),
RouterOSMenuSchema(
menuPath: "/interface wireless security-profiles", restPath: "interface/wireless/security-profiles", category: .wireless,
displayName: "WLAN-Sicherheitsprofile",
summary: "Verschlüsselung/Passwort-Vorlagen für WLAN-Interfaces (Legacy-Treiber).",
explanation: "Ein Profil wird angelegt und dann bei einem WLAN-Interface als \"security-profile\" eingetragen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. heimnetz-wpa2.", required: true),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: ["none", "static-keys-required", "dynamic-keys"]),
help: "dynamic-keys ist der übliche WPA/WPA2-Modus.", defaultValue: "dynamic-keys"),
RouterOSFieldSchema(key: "authentication-types", label: "Authentifizierung", kind: .enumPick(options: [
"wpa-psk", "wpa2-psk", "wpa-psk,wpa2-psk", "wpa-eap", "wpa2-eap"
]), help: "wpa2-psk = WPA2 mit gemeinsamem Passwort (Heimnetz-Standard)."),
RouterOSFieldSchema(key: "wpa2-pre-shared-key", label: "WPA2-Passwort", kind: .text, help: "Mindestens 8 Zeichen.")
],
listColumns: ["name", "authentication-types"]
),
RouterOSMenuSchema(
menuPath: "/interface wifi", restPath: "interface/wifi", category: .wireless,
displayName: "WLAN (neuer wifiwave2/802.11ax-Treiber)",
summary: "WLAN-Interfaces auf neueren Wireless-Chips.",
explanation: "Anderes, verschachteltes Konfigurationsschema als der Legacy-Treiber (Punkt-Notation wie \"security.passphrase\") — nicht mit \"/interface wireless\" mischen. Nur auf Geräten mit entsprechend neuem WLAN-Chip vorhanden.",
warning: "Diese App hat den .set-Pfad für den neuen Treiber noch nie gegen echte Hardware getestet (siehe HANDOFF.md) — nach dem Anwenden unbedingt prüfen.",
fields: [
RouterOSFieldSchema(key: "ssid", label: "Netzwerkname (SSID)", kind: .text,
help: "Der Name, den WLAN-Geräte in der Netzwerkliste sehen, z.B. MeinWLAN.", required: true),
RouterOSFieldSchema(key: "security.authentication-types", label: "Authentifizierung", kind: .enumPick(options: [
"wpa2-psk", "wpa3-psk", "wpa2-psk,wpa3-psk"
]), help: "wpa2-psk,wpa3-psk deckt sowohl ältere als auch neuere Geräte ab."),
RouterOSFieldSchema(key: "security.passphrase", label: "Passwort", kind: .text, help: "Mindestens 8 Zeichen."),
RouterOSFieldSchema(key: "datapath.bridge", label: "Ziel-Bridge", kind: .text,
help: "Name der Bridge, der dieses WLAN-Netz zugeordnet wird (üblicherweise dieselbe wie das kabelgebundene LAN), z.B. bridge."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "WLAN-Interface inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["ssid"]
),
generic("/caps-man manager", "caps-man/manager", .wireless, "CAPsMAN-Zentrale",
"Zentrale Verwaltung mehrerer WLAN-Access-Points von einem Router aus.",
"Nur relevant mit mehreren WLAN-Access-Points, die zentral verwaltet werden sollen — eigenes, umfangreiches Konfigurationsschema (Konfigurationen/Kanäle/Datapaths).")
]
// MARK: - Queues / QoS
private static let queueFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/queue simple", restPath: "queue/simple", category: .queues,
displayName: "Einfache Bandbreiten-Begrenzung",
summary: "Begrenzt Up-/Download einer IP-Adresse oder eines Netzes.",
explanation: "Reicht für die meisten Heim-/Kleinnetz-Fälle ohne Mangle-Markierungen aus.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. limit-gast.", required: true),
RouterOSFieldSchema(key: "target", label: "Ziel", kind: .text,
help: "IP-Adresse oder Netz, das begrenzt wird, mit Präfix, z.B. 192.168.88.50/32.", required: true),
RouterOSFieldSchema(key: "max-limit", label: "Max. Bandbreite (Upload/Download)", kind: .text,
help: "Zwei Werte getrennt durch \"/\", z.B. 10M/50M (Upload/Download).", required: true),
RouterOSFieldSchema(key: "burst-limit", label: "Burst-Bandbreite", kind: .text,
help: "Optional: kurzzeitig erlaubte höhere Bandbreite, z.B. 15M/60M."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Begrenzung inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["name", "target", "max-limit"]
),
RouterOSMenuSchema(
menuPath: "/queue tree", restPath: "queue/tree", category: .queues,
displayName: "Queue-Baum",
summary: "Erweiterte, hierarchische Bandbreiten-Steuerung basierend auf Mangle-Markierungen.",
explanation: "Statt einer festen Adresse wirkt ein Queue-Baum-Eintrag auf Verkehr mit einer bestimmten Mangle-Markierung.",
warning: "Setzt eine passende Mangle-Regel voraus, die genau diese Markierung setzt (siehe Firewall: Mangle) — ohne die greift ein Queue-Baum-Eintrag ins Leere.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. queue-gast.", required: true),
RouterOSFieldSchema(key: "parent", label: "Übergeordnet", kind: .text,
help: "Ein Interface (z.B. der WAN-Port, etwa ether1) oder der Name eines anderen Queue-Baum-Eintrags.", required: true),
RouterOSFieldSchema(key: "packet-mark", label: "Paket-/Verbindungsmarkierung", kind: .text,
help: "Name der Mangle-Markierung, auf die dieser Eintrag wirkt."),
RouterOSFieldSchema(key: "max-limit", label: "Max. Bandbreite", kind: .text, help: "Einzelwert, z.B. 20M."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Begrenzung inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["name", "parent", "max-limit"]
)
]
// MARK: - System
private static let systemFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/system identity", restPath: "system/identity", category: .system,
displayName: "Router-Name",
summary: "Der Name, unter dem sich der Router meldet (z.B. in Winbox/Terminal-Prompt).",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. MeinRouter.", required: true)
],
listColumns: ["name"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/system clock", restPath: "system/clock", category: .system,
displayName: "Uhrzeit/Zeitzone", summary: "", explanation: "",
fields: [
RouterOSFieldSchema(key: "time-zone-name", label: "Zeitzone", kind: .text,
help: "Z.B. Europe/Berlin.")
],
listColumns: ["time-zone-name"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/system ntp client", restPath: "system/ntp/client", category: .system,
displayName: "Zeitserver (NTP)",
summary: "Hält die Router-Uhr über einen Zeitserver synchron.",
explanation: "",
warning: "Falsche Systemzeit kann Zertifikatsprüfungen (HTTPS/REST) und Log-Zeitstempel durcheinanderbringen.",
fields: [
RouterOSFieldSchema(key: "enabled", label: "Aktiviert", kind: .bool,
help: "Zeitsynchronisation ein-/ausschalten.", defaultValue: "yes"),
RouterOSFieldSchema(key: "servers", label: "Zeitserver", kind: .text,
help: "Ein oder mehrere Server, kommagetrennt, z.B. pool.ntp.org.")
],
listColumns: ["servers"],
isSingleton: true
),
generic("/system logging", "system/logging", .system, "Protokollierung",
"Was der Router mitschreibt und wohin (Speicher, Datei, Remote-Syslog, E-Mail).",
"Besteht aus \"rules\" (was protokolliert wird) und \"actions\" (wohin) — hier generischer Zugriff, verwandte Teile über \"Eigener Menüpfad\" (z.B. \"/system logging action\")."),
RouterOSMenuSchema(
menuPath: "/system scheduler", restPath: "system/scheduler", category: .system,
displayName: "Zeitplaner",
summary: "Führt ein hinterlegtes Skript zu festen Zeiten/Intervallen aus.",
explanation: "",
warning: "Ein fehlerhaftes geplantes Skript kann unbeaufsichtigt wiederholt Änderungen vornehmen — vor dem Einplanen einmal manuell testen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. taeglicher-neustart.", required: true),
RouterOSFieldSchema(key: "start-time", label: "Startzeit", kind: .text, help: "Z.B. 00:00:00 oder \"startup\"."),
RouterOSFieldSchema(key: "interval", label: "Intervall", kind: .duration,
help: "Wie oft wiederholt wird. Alles auf 0 lassen = nur einmal zur Startzeit."),
RouterOSFieldSchema(key: "on-event", label: "Auszuführendes Skript",
kind: .menuItemPick(menuPath: "/system script", restPath: "system/script", valueField: "name"),
help: "Name eines zuvor unter \"Skripte\" angelegten Skripts."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Zeitplan inaktiv schalten, ohne ihn zu löschen.", defaultValue: "no")
],
listColumns: ["name", "interval"]
),
RouterOSMenuSchema(
menuPath: "/system script", restPath: "system/script", category: .system,
displayName: "Skripte",
summary: "Gespeicherte RouterOS-Befehlsfolgen, die manuell oder per Scheduler ausgeführt werden.",
explanation: "",
warning: "Skripte laufen mit den Rechten des Routers selbst — kein Unterschied zu manuell eingegebenen Befehlen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. backup-taeglich.", required: true),
RouterOSFieldSchema(key: "source", label: "Skript-Inhalt", kind: .text,
help: "RouterOS-Befehle, z.B. \":log info \\\"Test\\\"\".")
],
listColumns: ["name"]
),
RouterOSMenuSchema(
menuPath: "/user", restPath: "user", category: .system,
displayName: "Benutzerkonten",
summary: "Zugangskonten für den Router (Winbox/SSH/REST/Terminal).",
explanation: "",
warning: "Das zum Verbinden genutzte eigene Konto hier nicht versehentlich löschen oder herabstufen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Benutzername", kind: .text, help: "Frei wählbar, z.B. admin2.", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "Ausreichend lang und zufällig wählen."),
RouterOSFieldSchema(key: "group", label: "Rechte-Gruppe", kind: .enumPick(options: ["full", "write", "read"]),
help: "full = Vollzugriff, write = ohne Benutzerverwaltung, read = nur lesen.", defaultValue: "full"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Konto inaktiv schalten, ohne es zu löschen.", defaultValue: "no")
],
listColumns: ["name", "group"]
)
]
// MARK: - Tools
private static let toolFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/tool netwatch", restPath: "tool/netwatch", category: .tools,
displayName: "Netwatch",
summary: "Überwacht laufend, ob eine Adresse erreichbar ist, und kann bei Statuswechsel ein Skript auslösen.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "host", label: "Zu überwachende Adresse", kind: .text,
help: "IP-Adresse oder Hostname, ohne Präfix, z.B. 192.168.88.1 oder 8.8.8.8.", required: true),
RouterOSFieldSchema(key: "interval", label: "Prüf-Intervall", kind: .duration,
help: "Wie oft die Erreichbarkeit geprüft wird.", defaultValue: "10s"),
RouterOSFieldSchema(key: "up-script", label: "Skript bei Erreichbarkeit", kind: .text,
help: "Name eines Skripts unter \"Skripte\"."),
RouterOSFieldSchema(key: "down-script", label: "Skript bei Nichterreichbarkeit", kind: .text,
help: "Name eines Skripts unter \"Skripte\"."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Überwachung inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["host"]
),
RouterOSMenuSchema(
menuPath: "/tool e-mail", restPath: "tool/e-mail", category: .tools,
displayName: "E-Mail-Versand",
summary: "Postausgangs-Server, den Scheduler/Netwatch/Skripte für Benachrichtigungen nutzen.",
explanation: "Ein einzelner, geräteweiter Satz Einstellungen — kein Menü mit mehreren Einträgen.",
fields: [
RouterOSFieldSchema(key: "address", label: "Mailserver-Adresse", kind: .text,
help: "Hostname oder IP-Adresse des Mailservers, z.B. smtp.gmail.com.", required: true),
RouterOSFieldSchema(key: "port", label: "Port", kind: .int, help: "Meist 587 (STARTTLS) oder 465 (SSL)."),
RouterOSFieldSchema(key: "from", label: "Absenderadresse", kind: .text, help: "Z.B. router@example.com."),
RouterOSFieldSchema(key: "user", label: "Benutzername", kind: .text, help: "Zugangsdaten des Mailservers."),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "Zugangsdaten des Mailservers.")
],
listColumns: ["address"],
isSingleton: true
)
]
private static func generic(
_ menuPath: String,
_ restPath: String,
_ category: RouterOSMenuCategory,
_ displayName: String,
_ summary: String,
_ explanation: String = "",
warning: String? = nil
) -> RouterOSMenuSchema {
RouterOSMenuSchema(
menuPath: menuPath,
restPath: restPath,
category: category,
displayName: displayName,
summary: summary,
explanation: explanation.isEmpty ? summary : explanation,
warning: warning,
fields: []
)
}
}