Neues Schema für /system routerboard mode-button (enabled/on-event/ hold-time), live gegen echte Hardware verifiziert. Dabei drei echte Bugs gefunden und gefixt: - SSH-Trust-Dead-End beim ersten Experte-Tab-Schreiben (Backup-vor- Schreiben-Pfad hatte keinen Weg, den Trust-Dialog auszulösen) — ConnectionService.noteUntrustedSSHHostKey - RouterOSFieldSchema.clearable: optionale Felder senden nie mehr einen expliziten Leer-Wert, wenn das RouterOS ablehnt - RouterOSMenuSchema.writesRequireSSH + ConnectionService.applyViaSSH: für Menüs ohne REST-Anbindung (bewiesen per direktem SSH-Test) wird zwingend eine dedizierte SSH-Verbindung genutzt statt REST Live Ende-zu-Ende bestätigt: Skript anlegen, Mode-Taste zuweisen, Tastendruck löst Skript korrekt aus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
821 lines
62 KiB
Swift
821 lines
62 KiB
Swift
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, 2–4094, 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 routerboard mode-button", restPath: "system/routerboard/mode-button", category: .system,
|
||
displayName: "Mode-Taste",
|
||
summary: "Steuert, welches Skript beim Drücken der physischen Mode-Taste am Router ausgeführt wird.",
|
||
explanation: "Manche RouterBOARD-Geräte (z.B. hEX, cAP, hAP ac², LtAP mini, einige CCR/CRS) haben eine physische Mode-Taste an der Seite. Hier lässt sich festlegen, ob und wie lange sie gedrückt werden muss, damit ein zuvor unter \"Skripte\" angelegtes Skript ausgeführt wird.",
|
||
warning: "Ab RouterOS 7.1rc4 muss jede Aktivierung oder Änderung dieser Einstellung zusätzlich durch einen physischen Tastendruck (Reset- oder Mode-Taste) innerhalb von 60 Sekunden am Gerät selbst bestätigt werden — eine Änderung über diese App allein reicht nicht.",
|
||
fields: [
|
||
RouterOSFieldSchema(key: "enabled", label: "Aktiviert", kind: .bool,
|
||
help: "Mode-Taste ein-/ausschalten.", defaultValue: "no"),
|
||
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: "hold-time", label: "Haltedauer (Min..Max)", kind: .text,
|
||
help: "Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. \"3s..5s\". Verfügbar ab RouterOS 6.47beta60.",
|
||
clearable: false)
|
||
],
|
||
listColumns: ["enabled", "on-event"],
|
||
isSingleton: true,
|
||
writesRequireSSH: 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: "Die Server-Liste selbst liegt in einem eigenen Menü (\"NTP-Zeitserver-Liste\") — hier nur Ein/Aus und Modus.",
|
||
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: "mode", label: "Modus", kind: .enumPick(options: [
|
||
"unicast", "broadcast", "multicast", "manycast"
|
||
]), help: "Fast immer \"unicast\" (direkte Anfrage an feste Server).", defaultValue: "unicast")
|
||
],
|
||
listColumns: ["enabled", "mode"],
|
||
isSingleton: true
|
||
),
|
||
RouterOSMenuSchema(
|
||
menuPath: "/system ntp client servers", restPath: "system/ntp/client/servers", category: .system,
|
||
displayName: "NTP-Zeitserver-Liste",
|
||
summary: "Die Zeitserver, die der Client abfragt.",
|
||
explanation: "\"servers\" ist bei RouterOS 7.x kein Feld am NTP-Client selbst, sondern eine eigene Liste — jeder Server ist ein eigener Eintrag hier (statt kommagetrennt in einem Textfeld).",
|
||
fields: [
|
||
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text,
|
||
help: "Hostname oder IP eines Zeitservers, z.B. pool.ntp.org.", required: true),
|
||
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
|
||
help: "Diesen Server deaktivieren, ohne ihn zu löschen.", defaultValue: "no"),
|
||
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur eigenen Wiedererkennung.")
|
||
],
|
||
listColumns: ["address", "disabled"]
|
||
),
|
||
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: []
|
||
)
|
||
}
|
||
}
|