VPN Peers & Clients
Überblick
Peers lassen sich auf zwei Arten organisieren: Tags (frei wählbare, many-to-many-Labels) und Peer-Gruppen (strukturierte one-to-many-Zuordnung). Beide Systeme teilen sich die Settings-Seite, haben aber unterschiedliche Datenmodelle und Lebenszyklen.
Tags waren historisch reine CSV-Strings in peers.tags — der Admin hat ein Komma-separiertes Feld bearbeitet, das war alles. Mit v1.48 kam die Registry-Tabelle tags dazu, damit Tag-Namen zentral verwaltbar sind: neu anlegen, umbenennen, löschen (inklusive Strip aus allen Peer-CSVs), Usage-Counts in der Admin-UI. Das Hybrid-Modell lebt weiter: Die CSV pro Peer ist weiterhin die Quelle für die Zuordnung, die Registry ist die zentrale Liste.
Peer-Gruppen sind das jüngere, einfachere System: Eine Tabelle, ein Foreign-Key in peers, Display-Farbe, Beschreibung. Gruppen sind für dauerhafte Rollen gedacht (z.B. "Admin", "Familie", "Mobil"), Tags für ad-hoc Labels (z.B. "Desktop", "nas-stack", "Test").
Tags
Datenmodell
-- Registry (ab Migration 39)
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_tags_name ON tags(name);
-- Peer-seitig (ab Migration 10)
ALTER TABLE peers ADD COLUMN tags TEXT DEFAULT '';
-- CSV-Form: "nas, desktop, prod"
Die Registry speichert Tag-Namen im originalen Case, wie der Admin sie zuerst eingegeben hat. Uniqueness gegen bestehende Registry-Einträge läuft case-insensitive über COLLATE NOCASE. Sprich: "NAS" und "nas" landen in einer einzigen Registry-Zeile mit dem Original-Case "NAS".
Warum Hybrid statt reine M:N-Tabelle?
Eine klassische peer_tags(peer_id, tag_id)-Tabelle wäre sauberer, hätte aber bedeutet, die vorhandene UI, die Imports, die Backup-Exports und alle API-Clients neu zu schreiben. Statt Migration-Zwang hält die Registry einen eigenen Lebenszyklus und der Peer-CSV bleibt unangetastet:
| Eigenschaft | CSV | Registry |
|---|---|---|
| Zuordnung Peer↔Tag | Ja, pro-Peer | Nein, nur zentrale Liste |
| Reine Orphans (Tag ohne Peer) | Nein | Ja |
| Umbenennen | Würde viele Peers schreiben | Einfach (eine Registry-Zeile) — aber nicht implementiert |
| Delete-Propagation | — | Strip aus allen Peer-CSVs |
| Auto-Registration beim Peer-Save | — | Ja (ensureRegistered) |
Tag-Name-Validierung
src/services/tags.js:validateName:
- Max 64 Zeichen.
- Keine
,(CSV-Parser würde tokenisieren), keine< > "(HTML-Safety), keine\n \r \t(Newline-Injection).
Das Feld wird vor Registry-INSERT und vor Peer-CSV-Write geprüft.
list() — Merge-View
Die Admin-UI zeigt eine zusammengeführte Liste: alle Registry-Einträge UND alle tatsächlich vergebenen Tags, mit einem registered-Flag und dem peer_count:
[
{id: 7, name: "NAS", peer_count: 3, registered: true },
{id: null, name: "legacy", peer_count: 1, registered: false },
{id: 12, name: "pool", peer_count: 0, registered: true }
]
id=null, registered:false— Tag taucht auf Peer-CSVs auf, ist aber nicht in der Registry. Die UI kann anbieten, ihn zu registrieren.peer_count=0, registered:true— Orphan-Tag. Admin kann löschen ohne Peers zu betreffen.
Sortierung: localeCompare mit sensitivity:'base' — case-insensitive, aber Locale-aware.
Auto-Registration
Bei jedem peers.create und peers.update ruft der Peer-Service tags.ensureRegistered(data.tags) auf. Die Funktion:
- Akzeptiert CSV-String oder Array.
- Filtert jede Token gegen
validateName-Regeln; invalide Tokens werden stumm übersprungen (ein kaputtes Token blockiert nicht das Peer-Save). INSERT OR IGNORE— idempotent.- Läuft in einer Transaktion.
Ziel: Ein Admin, der "NAS, prod" in die Peer-Form tippt, sieht beide Tags sofort in der Registry-View; kein extra Setup-Schritt.
Startup-Backfill
tags.backfillFromPeers() wird einmalig beim Server-Boot (in server.js) aufgerufen:
- Scannt
peers.tagsnach distinct Tokens. - Registriert alle, die noch nicht drin sind.
Zweck: Nach dem Upgrade auf die Registry-Version haben Bestandsinstallationen sofort einen gefüllten Registry-Katalog, ohne dass der Admin etwas tun muss.
remove(name) — die interessante Operation
Tag löschen bedeutet: Registry-Eintrag weg UND Token aus allen Peer-CSVs strippen.
DELETE FROM tags WHERE name = ? COLLATE NOCASE;
-- Dann in einer Transaktion:
for each peer with non-empty tags:
tokens = splitCsv(peer.tags)
filtered = tokens.filter(t => t.toLowerCase() !== name.toLowerCase())
if filtered.length < tokens.length:
UPDATE peers SET tags = filtered.join(', ') WHERE id = peer.id
Zwei Subtilitäten:
- Whole-Token-Match — Tag "prod" löschen darf nicht Token "production-backup" treffen. Der CSV wird token-weise verglichen, nicht per SQL
LIKE '%prod%'. - Case-insensitive Vergleich, Case-erhaltende Peers — der Peer hat
"NAS, Prod", wir löschen"nas". Das Ergebnis ist"Prod"(Case bleibt), "NAS" ist weg.
Activity-Log: tag_deleted mit peers_affected und removed_from_registry.
splitCsv(csv) — der Parser
- Splittet auf
,, trimmt jeden Token. - De-dupliziert case-insensitive, behält aber den ersten gesehenen Case.
- Leere Tokens werden gedroppt.
So wird "NAS, nas, PROD, ," zu ["NAS", "PROD"].
UI
- Settings → Tags-Admin-Karte (
public/js/tags-admin.js) — neben der Peer-Gruppen-Karte. - Rendert die Merge-View aus
GET /api/v1/tags. - Create-Input + Delete-Button pro Zeile.
- Peers-Seite: Autocomplete aus Registry, beim Speichern kommt die CSV-Zeile in
peers.tags.
Peer-Gruppen
Datenmodell
CREATE TABLE peer_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#6b7280',
description TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- FK auf peers (Migration 25)
ALTER TABLE peers ADD COLUMN group_id INTEGER
REFERENCES peer_groups(id) ON DELETE SET NULL;
CREATE INDEX idx_peers_group_id ON peers(group_id);
Ein Peer hat genau eine Gruppe oder keine (NULL). ON DELETE SET NULL: Wird eine Gruppe gelöscht, bleiben die Peers bestehen, nur ihre group_id wird auf NULL gesetzt — Peers verschwinden nicht zusammen mit der Gruppe.
Farbe
color ist ein Hex-String (#rrggbb), wird in der UI als Farb-Chip neben dem Peer-Namen angezeigt. Default #6b7280 (Grau).
API und UI
GET/POST/PATCH/DELETE /api/v1/peer-groups— Standard-CRUD (src/routes/api/peerGroups.js).- Settings → Peer-Gruppen-Karte (
public/js/peer-groups-admin.js). - Peers-Edit-Form hat ein Dropdown "Gruppe" mit allen Gruppen + "— keine —".
Tags vs Gruppen — wann was
| Kriterium | Tags | Gruppen |
|---|---|---|
| Kardinalität | many-to-many | one-to-many |
| Lebenszyklus | kurzlebig, ad-hoc | dauerhaft, strukturell |
| Typische Use-Cases | "Desktop", "Test", "backup-2026" | "Admin", "Familie", "Mobil" |
| Delete-Verhalten | Wird aus allen Peers entfernt | Peers bleiben, group_id=NULL |
| Farbe | — | Ja |
| Beschreibung | — | Ja |
Ein Peer ist z.B. in Gruppe "Familie", aber getaggt mit "Desktop, nas-backup". Die Gruppe bestimmt Default-Rollen (evtl. Route-Auth-Regeln in Zukunft), die Tags sind für Filter und Anzeige.
3. Peers / Clients verwalten
Die Seite Peers & Clients ist zweigeteilt:
- Home Gateways — oben, als aufklappbare Karten mit Telemetrie.
- Peers / Clients — unten, als Tabelle mit Suche, Gruppen-Filter und Tag-Filter. Gateway-Peers werden aus dieser Tabelle herausgefiltert, damit du sie nicht doppelt siehst.
3.1 Neuen Peer anlegen
Öffne den + Hinzufügen-Dialog. Die wichtigsten Felder:
- Name — eindeutig, 1–63 Zeichen, a-z/0-9/Bindestrich. Wird auch als WireGuard-Peer-Bezeichnung benutzt.
- Beschreibung — frei, max. 255 Zeichen. Hier landet z.B. das Client-Version-Label vom GateControl-Desktop-Client automatisch.
- Interner Hostname (Pro) — siehe Domains & DNS. Nur wenn du Internal-DNS nutzt.
- Gruppe — optional, siehe 3.4 Gruppen.
- Tags — kommagetrennt; z.B.
server, produktion, nas. - DNS-Server (Override) — überschreibt den globalen DNS-Eintrag nur für diesen Peer. Bleibt das Feld leer, gilt der globale Wert aus Settings.
- Ablaufdatum — nach diesem Datum wird der Peer automatisch deaktiviert (Hintergrundjob alle 60 Sekunden).
- Home Gateway (Checkbox) — macht aus dem Peer einen Gateway (siehe 4. Home-Gateway). Nicht nachträglich änderbar.
Nach Anlegen bekommst du Config, QR-Code und — bei Gateways — zusätzlich
die gateway.env als einmalige Anzeige.
3.2 Hostname setzen (Internal DNS)
In der Peer-Tabelle taucht in der Zeile pro Peer eine kleine Badge auf:
manuell (vom Admin gesetzt), auto (vom Agent gemeldet) oder veraltet
(nach Master-Key-Rotation noch nicht neu gemeldet).
Sticky-Admin-Regel: Wenn du als Admin den Hostname gesetzt hast, überschreibt kein Agent-Heartbeat mehr diesen Wert. Möchtest du wieder automatische Übernahme, lösche den Hostname im Peer-Modal — dann darf der nächste Agent-Heartbeat ihn wieder setzen.
Agent-gesetzte Namen kannst du als Admin jederzeit überschreiben. Das Flag
kippt dann auf manuell und bleibt dort.
3.3 Tags
Tags sind freie, nicht-hierarchische Labels. Verwalten kannst du sie unter
Settings → Tags. Dort hast du eine Registry — angelegte Tags können so
bereits autovervollständigt werden. Ein Tag, das an Peers hängt aber nicht in
der Registry steht, zeigt das Badge nicht registriert.
Verwendung:
- In der Peer-Tabelle kannst du nach Tag filtern (Chip-Leiste oberhalb der Tabelle).
- Im Peer-Modal trägst du Tags kommagetrennt ein.
- Beim Speichern werden unbekannte Tags automatisch in die Registry aufgenommen, damit sie künftig in Autocomplete erscheinen.
3.4 Gruppen
Peer-Gruppen sind sichtbare Kategorien mit Namen, Farbe und Beschreibung (z.B. "Büro", "Familie", "Server"). Jeder Peer gehört zu genau einer oder zu keiner Gruppe.
- Gruppen anlegen und pflegen: Settings → Peer-Gruppen
- Peer zuweisen: im Peer-Dialog Dropdown Gruppe
- Filter: auf der Peers-Seite im Dropdown Alle / Nicht gruppiert / <Gruppe>
3.5 Deaktivieren vs. Löschen
- Deaktivieren (Toggle in der Peer-Zeile, oder Batch-Aktion):
- Peer bleibt in der DB, Keys bleiben erhalten.
- WireGuard-Config wird ohne diesen Peer neu geschrieben, die aktive Verbindung wird sofort getrennt.
- Kann jederzeit wieder aktiviert werden.
- Löschen (Mülleimer-Icon, mit Bestätigung):
- Peer wird endgültig entfernt, Keys sind weg.
- Zu diesem Peer gehörende Routen zeigen einen
peer offline-Badge — aktive Verbindungen werden getrennt. - Nicht rückgängig zu machen. Wenn du den Peer neu anlegst, bekommt er eine neue IP und neue Keys.
3.6 Ablaufdaten
Im Peer-Dialog unter Ablaufdatum kannst du 1d / 7d / 30d / 90d wählen oder ein eigenes Datum. Nach Ablauf wird der Peer automatisch deaktiviert. In der Tabelle siehst du:
Läuft bald ab(gelbes Badge, < 7 Tage)Abgelaufen(rotes Badge, bereits deaktiviert)
3.7 Batch-Aktionen
Oben rechts auf Auswählen, dann Checkboxen in den Zeilen. Unten erscheint eine Aktionsleiste mit Aktivieren, Deaktivieren, Löschen.