Deep Dive
NetBird selbst betreiben: der Vermittler, der zur Seite tritt
Ein kleiner Server bei Hetzner stellt alle meine Geräte einander vor und hält sich dann raus: was darauf läuft, wie ich ihn gefahrlos aktualisiere und welche Fallen mich Stunden gekostet haben.
Auf jeder guten Party gibt es diese eine Person, die alle kennt. Sie nimmt dich am Arm, sagt „Das ist Jana, sie baut auch gerade ein Gartenhaus“, und zwei Minuten später redet ihr über Fundamente, während sie längst beim nächsten Grüppchen steht. Für euer Gespräch braucht ihr sie nicht mehr. Ihr hättet euch ohne sie nur nie gefunden.
Genau so eine Person betreibe ich, als kleinen Server bei Hetzner. Gemerkt habe ich das im September. Ich startete ein Upgrade, und für eine Viertelminute stand auf dem Server alles still: keine Verwaltung, kein Dashboard, keine Anmeldung. Im Homelab merkte das niemand. Die VMs redeten weiter miteinander, und das Handy behielt seinen Weg nach Hause.
In Teil 2 habe ich NetBird als das Netz vorgestellt, über das alle meine Geräte verschlüsselt miteinander sprechen. Hier geht es um den Teil, den man sonst nicht sieht: den Vermittler selbst. Was er weiß, was auf ihm läuft, wie zwei Geräte über ihn zueinander finden, und warum es ihn nicht kümmert, wenn er kurz fehlt.
Ein Server, der nur vorstellt
NetBird trennt zwei Dinge, die man leicht in einen Topf wirft. Die Steuerung, auf Englisch Control Plane, weiß, wer dazugehört und wer mit wem reden darf. Die Daten selbst laufen durch WireGuard-Tunnel direkt zwischen den Geräten, die bei NetBird Peers heißen. Mein Server macht nur das Erste. Jeder Peer erzeugt seinen Schlüssel selbst und meldet nur den öffentlichen Teil an, also kann der Server nicht in die Tunnel hineinsehen.
Harmlos ist er trotzdem nicht. Auf ihm liegt die Liste aller Geräte, Gruppen und Regeln, dazu die Setup-Keys und die Anmeldedaten. NetBird gibt es als Cloud-Dienst, der Server ist aber quelloffen, und ich wollte diese Liste auf einer Maschine haben, die ich selbst verwalte, ohne Konto bei einem Anbieter. Dafür bleiben Updates, Sicherung und Fehlersuche an mir hängen, und die Fehlersuche war nicht immer ein Vergnügen.
Was auf dem kleinen Server läuft
Die Maschine ist ein kleiner Debian-Server bei Hetzner, und alles darauf ist ein einziger Docker-Compose-Stack unter /srv/netbird. Ganz vorne steht Traefik, ein Reverse Proxy, der Zertifikate bei Let’s Encrypt holt und jede Anfrage auf Port 443 anhand ihres Pfads weiterreicht. Die eigentliche Arbeit macht ein einziger Container, netbird-server. In den neueren Versionen steckt dort alles drin, was früher getrennt lief.
| Baustein | Was er tut |
|---|---|
| Management | kennt Peers, Gruppen, Regeln, DNS und Routen und schiebt jede Änderung an die Peers |
| Signal | ein Briefkasten, über den zwei Peers ihre Adressen austauschen |
| Relay | reicht verschlüsselte Pakete durch, wenn kein direkter Weg klappt |
| STUN | sagt einem Peer, unter welcher öffentlichen Adresse er von außen zu sehen ist |
| Dashboard | die Weboberfläche, eigener Container |
Daneben laufen der Reverse Proxy von NetBird, mit dem man interne Dienste ins Internet stellen kann, und ein paar kleine nginx-Container von mir, zu denen ich bei den Fallen komme. Der ganze Zustand steckt in SQLite-Dateien in einem Docker-Volume: Peers und Regeln, die Logins des eingebauten Identity Providers und ein Ereignisprotokoll.
netbird-server macht die eigentliche Arbeit.Dieser Server ist der einzige Teil meines Netzes mit offenen Ports, 443 für fast alles und zwei UDP-Ports. Selbst das Relay läuft über HTTPS, unter /relay. So viel zum Gebäude. Interessanter wird es, wenn man zusieht, was darin passiert, wenn zwei Peers sich zum ersten Mal begegnen.
Wie zwei Peers sich finden
Nehmen wir das Handy, das zu proxy-01 will. Beide sitzen hinter einem Router mit NAT, also einer Adressübersetzung, die Verbindungen von außen abweist, solange niemand von innen angefangen hat. Keiner von beiden kann den anderen einfach anrufen. Wie auf der Party braucht es jemanden, der vorstellt.
Zuerst sammelt jeder Peer, wo er vielleicht erreichbar ist. Seine Adresse im eigenen Netz heißt Kandidat host. Seine öffentliche Adresse erfährt er vom STUN-Dienst, das ist der Kandidat srflx. Und als letzter Ausweg bleibt das Relay. Diese Listen tauschen die beiden über Signal aus, verschlüsselt mit ihren eigenen Schlüsseln.
Dann wird ausprobiert. ICE, ein Verfahren aus der Videotelefonie, schickt von beiden Seiten gleichzeitig Prüfpakete an alle Kandidaten. Weil beide von innen senden, öffnet jeder Router ein kleines Loch für die Antwort, und das beste Paar, das durchkommt, gewinnt. Klappt gar nichts, springt das Relay ein. Die Pakete sind dann schon von WireGuard verschlüsselt, das Relay sieht nur Rauschen.
Steht der Tunnel einmal, braucht er den Server nicht mehr. Das erklärt das ruhige Upgrade im September. In der Pause gehen nur Dinge nicht, die neu ausgehandelt werden müssen: ein neuer Peer, eine geänderte Regel, ein Handy, das vom WLAN ins Mobilnetz wechselt. Welcher Weg gewonnen hat, zeigt netbird status -d für jeden Peer. Zwischen meinen VMs steht dort P2P mit host/host, also direkt durchs LAN. Wie oft das Relay unterwegs einspringt, habe ich ehrlich gesagt nie gemessen.
Ordnung im Netz: Rollen, Schlüssel, DNS
Die Party funktioniert nur, wenn der Gastgeber weiß, wer zu wem passt. Bei mir hat jeder Peer genau eine Rolle und genau eine DNS-Gruppe, und mit diesen zwei Achsen bleibt das Regelwerk klein. Die Rollen heißen mobile, lan-servers, lan-infra und remote-servers. Dazu kommt eine Gruppe lan-home ganz ohne Peers. Sie trägt nur eine Ressource, also ein Netz, dessen Geräte selbst kein NetBird haben, in diesem Fall das Heim-LAN.
Wer mit wem reden darf, regeln vier Policies:
| Policy | Wer → wohin | Erlaubt |
|---|---|---|
home-mesh | mobile, lan-servers, lan-infra untereinander | alles |
home-to-remote-servers | die drei Heimgruppen → remote-servers | nur einzelne TCP-Ports |
remote-to-lan-services | remote-servers → lan-servers | nur InfluxDB und MQTT |
mobile-to-lan-home | mobile → Ressource lan-home | das Heim-LAN |
Für jede Rolle gibt es einen Setup-Key, wiederverwendbar und ein Jahr gültig. Er steckt einen neuen Peer automatisch in seine Rolle und in seine DNS-Gruppe, ich klicke dafür nichts im Dashboard. Eine neue VM tritt bei ihrer Einrichtung mit netbird up --management-url https://netbird.example --setup-key … bei. Die Management-URL habe ich anfangs vergessen. Dann meldet sich der Client nämlich beim Cloud-Dienst an, der meinen Schlüssel nie gesehen hat, und antwortet knapp mit setup key is invalid.
Bei DNS ist dns-adguard der Normalfall: AdGuard auf dns-01, dahinter die Replik auf dem Raspberry Pi. Der Pi selbst fragt Quad9, denn ein AdGuard-Host kann sich nicht selbst als Nameserver nehmen. Der Backup-Server bei Hetzner bekommt überhaupt keine Regel, weil er AdGuard nicht erreicht. NetBird trägt nur seinen eigenen Resolver ein, ohne Rückfall, und ohne erreichbaren Nameserver stünde er ohne DNS da. Wie ich das mit einer Regel für die Gruppe All am eigenen Leib erlebt habe, steht in Teil 2.
Das Gateway
Ein Peer hat bei mir eine Sonderrolle. Eine kleine VM auf pve1 stellt dem Handy das Heim-LAN bereit und ist Exit-Node, das heißt: Wenn ich will, geht unterwegs mein gesamter Verkehr über zu Hause ins Internet. Weiterleitung und NAT richtet NetBird selbst ein. Fällt das Gateway aus, übernimmt der Pi. Dieselbe Route trägt dafür beide als Router, mit einer Metrik, bei der die kleinere Zahl gewinnt: 100 für das Gateway, 200 für den Pi.
Die Exit-Route gilt nur für mobile, und dabei bleibt es. Bekommt ein Server mit öffentlicher Adresse so eine Route, wandert seine Standardroute in den Tunnel, und von außen ist er tot. Dann hilft nur noch die Konsole beim Hoster. Seitdem habe ich immer einen Notzugang, der nicht von NetBird abhängt.
Zurück zum Upgrade
Damit sind wir wieder beim Upgrade im September. Auf dem Server lief noch Version 0.72.2, aktuell war 0.78. Sechs Versionssprünge auf einmal, und in der SQLite-Datenbank steckt jeder Peer, jeder Schlüssel und jede Regel. Wenn beim Start der neuen Version eine Datenbankmigration schiefgeht, darf ich im schlimmsten Fall jedes Gerät neu anmelden. Das war mir nicht geheuer, also habe ich Backup und Upgrade in ein Skript gegossen, das beides in der richtigen Reihenfolge erledigt.
Das Skript kopiert zuerst compose-backup auf den Server, dasselbe kleine Werkzeug, das in Teil 6 alle Docker-VMs sichert. Es stoppt den Stack für etwa 15 Sekunden, sichert Volumes und Konfiguration samt Prüfsummen und startet ihn wieder. Das waren die 15 Sekunden, die keiner bemerkt hat. Danach wird die Sicherung geprüft, auf meinen Arbeitsrechner gespiegelt, dort gegen die Prüfsummen verglichen und nachts mit in den S3-Speicher genommen.
Erst dann kommt das eigentliche Upgrade. Die laufenden Images bekommen vorher ein Etikett pre-upgrade-<datum>, denn nach dem Ziehen zeigt :latest auf die neue Version, und die alte gäbe es sonst nur noch in der Registry. Nach dem Start sucht das Skript im Log nach Fehlern und gescheiterten Migrationen, ruft Dashboard und Anmeldung per HTTP auf und fragt die API, wie viele Peers verbunden sind.
Die Version lese ich aus dem Log, aus der Zeile management server version. Das Etikett am Image sagt bei :latest gar nichts. Am 7. September stand dort 0.78.1, das Dashboard war bei v2.92.0. Gebraucht habe ich den Rückweg nicht, aber er liegt bereit:
compose-backup restore backups/<stand> --force --pinned --up
Fallen, die wie etwas anderes aussahen
Bleiben die Stunden, die ich mit Fehlersuche verbracht habe. Das Gemeine an allen drei Fallen war, dass sie sich als etwas anderes verkleidet haben.
NetBird-SSH und Port 22
NetBird bringt eine eigene SSH-Funktion mit, bei der man sich mit dem NetBird-Login anmeldet statt mit einem Schlüssel. Ist sie auf einem Peer an, leitet er Port 22 seiner NetBird-Adresse per NAT auf NetBirds eigenen SSH-Server um. Ein ganz normales ssh landet dann dort, und der echte SSH-Dienst des Hosts bekommt den Versuch nie zu sehen. Seit September ist die Funktion auf allen Peers per API abgeschaltet.
Peer-Namen gibt es zweimal
NetBird gibt jedem Peer einen Namen wie pve1.peers.netbird.example, und nur der NetBird-Resolver kennt ihn. Meine öffentliche Zone hat aber einen Wildcard-Eintrag. Fragst du denselben Namen über die Fritz!Box, AdGuard oder Quad9 ab, landest du beim NetBird-Server bei Hetzner, und der verlangt bei SSH ein Passwort, das nie passt. Peer-Namen löse ich deshalb nur über NetBird auf, oder ich nehme gleich die NetBird-Adresse.
Aus der Praxis: Vom Laptop kam ich plötzlich nicht mehr per SSH auf meinen Arbeitsrechner. „Permission denied“, obwohl Schlüssel und Passwort stimmten. Ich habe lange am Zielrechner gesucht, bis mir auffiel, dass dessen SSH-Log überhaupt keinen Versuch zeigte. Ich hatte die ganze Zeit mit jemand anderem gesprochen. Heute vergleiche ich bei so etwas als Erstes den Fingerabdruck des Host-Schlüssels.
Ein Impressum im Dashboard
Weil der Server öffentlich erreichbar ist, braucht er ein Impressum. Ins Dashboard-Image wollte ich dafür nicht eingreifen, die Änderung wäre beim nächsten Upgrade weg. Die Seite liefert deshalb ein eigener nginx-Container aus, und Traefik schickt /impressum mit höherer Priorität dorthin als den Rest ans Dashboard. Den Link im Dashboard setzt ein dünner Proxy davor, der vor </body> ein kleines Skript einbindet. Das ging erst im dritten Anlauf. Ein eingefügtes Stück HTML räumt die React-App beim Laden wieder weg, ein Inline-Skript blockiert ihre eigene Content-Security-Policy, eine externe Skriptdatei vom selben Server lässt sie durch.
Aus der Praxis: Auf der Login-Seite stand der Link, und jede Prüfung war grün. Im Dashboard nach der Anmeldung fehlte er trotzdem, zehn Tage lang unbemerkt. Schuld war meine Traefik-Regel
PathPrefix(/impressum), ein reiner Textvergleich ohne Pfadgrenze, der auch/impressum-link.jseinfing. Der Browser bekam statt des Skripts die Impressumsseite als HTML und verwarf sie kommentarlos. Aufgefallen ist mir das erst, als ich mir die Antwort auf genau diese Datei angesehen habe. Die Regel heißt jetztPath(/impressum) || PathPrefix(/impressum/).
Was ich mitnehme
Am meisten hat mir geholfen, Steuerung und Daten getrennt zu denken. Damit wird fast jedes Verhalten von NetBird verständlich, auch das ruhige Upgrade. Rollen auf zwei Achsen mit einem Setup-Key pro Rolle sparen mir jede Klickerei, und vor einem Upgrade kommt immer erst das Backup. Und wenn etwas aussieht wie ein Anmeldefehler, prüfe ich zuerst, ob ich überhaupt beim richtigen Dienst ankomme.
Der Gastgeber hat seine Arbeit also getan, sobald alle sich kennen. Was die Gäste danach miteinander anstellen, wie das Handy über AdGuard, Proxy und Wildcard-Zertifikat bis zu Paperless kommt, erzählt Teil 2: Netz & Zugriff.