Hintergrund-Informationen zu netBiDiB

Die Protokollspezifikation zu netBiDiB entstand aus der Absprache zwischen Steuerungsherstellern, Knotenentwicklern und Anwendern unter der Berücksichtigung und Abwägung der Belange Sicherheit, Anwenderfreundlichkeit, Funktionsumfang und Implementierungsaufwand. Auf dieser Seite sind Erläuterungen zu den Designentscheidungen sowie eine nähere Beleuchtung der Funktionalität als FAQ zu finden.

Warum werden sowohl UDP als auch TCP verwendet?

BiDiB hat das Ziel, eine fehlerfreie Kommunikation zur Modellbahn zu realisieren. Dies ist nicht nur für zuverlässigen Regelbetrieb sondern gerade auch bei Firmwareupdates wichtig. UDP hat keine Auslieferungsgarantie und auch keine garantierte Einhaltung der Reihenfolge, was insbesondere im Funkbetrieb via WLAN zu einer hohen Fehlerrate führt.

BiDiB hat zwar mit den Nachrichtensequenznummern bereits eine Ende-zu-Ende-Absicherung und wichtige Nachrichten werden durch automatische Wiederholung bei ausbleibender Bestätigung gesichert, jedoch ist dies für seltene Störungen vorgesehen und nicht für fehleranfällige Transportkanäle ausgelegt. Daher ist eine zusätzliche Sicherungsschicht auf der individuellen Transportebene nötig und vermindert auch den Aufwand einer applikationsspezifischen Absicherung.

Deshalb wählten wir (trotz höheren Speicher- und Prozessorleistungsbedarfs gerade auf kleineren Baugruppen) die Kommunikation via TCP. Andererseits bietet TCP keinen Broadcast, den wir für ein anwenderfreundliches automatisches Finden von Baugruppen benötigen.

Also wurde im Discovery-Schritt UDP verwendet, die restliche Kommunikation geht dann über TCP.

Warum wurde auf Verschlüsselung verzichtet?

Eine Kommunikation im Internet (insbesondere, wenn dabei auch ein Firmwareupdate möglich ist) muss aus heutiger Sicht zwingend verschlüsselt sein. Allerdings bedeutet eine sichere Implementierung erheblichen Aufwand und ein umfassendes Sicherheitskonzept, das bei Abdeckung aller Aspekte an vielen Stellen eine Einschränkung bedeutet.

Eine Modellbahnsteuerung wird jedoch in der Regel im abgesicherten lokalen Heimnetzwerk betrieben, es findet kein Datenverkehr durch das Internet statt. Wo dies erforderlich ist, kann ein Virtual Private Network (VPN) den nötigen Schutz bieten.

Auch wenn der Einfachheit halber zurzeit keine Verschlüsselung erforderlich ist, wurde die Architektur bereits so angelegt dass sie auch mit abgesicherten Verbindungen verwendet werden kann.

Wozu ist das Pairing notwendig?

Ein netBiDiB-System soll sich 'von selbst' verbinden können, aber nur mit den vom Anwender gewünschten Geräten. Dies soll auch dann funktionieren, wenn sich mehrere unabhängige netBiDiB-Systeme im selben Netz befinden, was z. B. auf Modellbahnertreffen oder in Vereinen mit mehreren Anlagen(teilen) der Fall sein kann.

Wer soll sich nun wohin verbinden? Mit dem Pairing haben wir eine komfortable Möglichkeit geschaffen, auch in solchen Umgebungen den 'eigenen' Anlagenteil zusammenzuhalten und zugleich die automatische Verbindung zuzulassen.

Wieso benötigt auch der Host eine UID fürs Pairing?

Beim Pairing merkt sich nicht nur der Host die verbundenen Knoten, sondern auch die Knoten merken sich ihrerseits den bevorzugten Verbindungspartner. So wird unter anderem verhindert, dass ein handelsübliches Hostprogramm zur 'feindlichen Übernahme' eines Systems verwendet werden kann - weder versehentlich noch absichtlich. Die verschiedenen Programminstanzen benötigen dazu natürlich ein Unterscheidungsmerkmal, die Verwendung der Unique-ID-Infrastruktur von BiDiB war naheliegend.

Wie wird die UID des Hosts gewählt?

Die Unique-ID einer Hostprogramm-Instanz soll möglichst global eindeutig sein, dies lässt sich aber nicht ohne eine zentrale Vergabestelle realisieren. Eine derartige Infrastruktur ist jedoch unverhältnismäßig aufwändig, wo sie nicht bereits vorhanden ist (etwa für die Vergabe von Programmlizenzen) soll daher auf einen Zufallsgenerator ausgewichen werden. Eine zufällig generierte Seriennummer wird entweder in den Programmeinstellungen oder in der Anlagendatei gespeichert, um sich nicht bei jedem Programmstart zu ändern. Alternativ kann die Seriennummer auch mittels einer Hashfunktion aus einer eindeutigen Kennung des Computers (z.B. MAC-Adresse) erzeugt werden.

Wie bei Knoten beginnt die Unique-ID mit einer Herstellerkennung (Vendor ID), jeder Hersteller kann über seinen Nummernraum frei verfügen und eine entsprechende Vergabemethode wählen. Programmanbieter ohne NMRA-Kennung können eine 16-Bit Produktkennung aus dem Bereich für Open-Source-Komponenten (VID 13) erhalten.

Wie wird der Name im Descriptor eines Hosts gewählt?

Der Descriptor dient dem Anwender zur leichten Unterscheidung mehrerer Verbindungen, ohne nur die UID zum Abgleich verwenden zu können. Auf einem Gerät mit GUI, z. B. einem Handregler oder Konfigtool, kann der Anwender etwa auswählen an welchem von mehreren verfügbaren Hosts sich der Knoten anmelden soll. Der Produkt- und Username sollten daher möglich aussagekräftig sein.

Als Produktname soll der Programmname verwendet werden, der Username kann entweder vom Anwender direkt einstellbar sein oder automatisch aus Computernamen, Betriebssystem-Login oder dem Namen der geöffneten Anlagendatei erzeugt werden.

Der Pairing-Prozess ist viel zu kompliziert.

Der Linkstatus kann durch einen einfachen asynchronen Zustandsautomaten repräsentiert werden. Dieser hat nur 5 Zustände:

  • null: der Link wurde noch nicht initialisiert, das Gegenüber ist nicht bekannt
  • unpaired: beide Seiten vertrauen sich nicht (oder wissen es nicht)
  • their-request: die Gegenseite vertraut dem Teilnehmer, umgekehrt aber nicht (es wird noch auf den Anwender gewartet)
  • my-request: der Teilnehmer vertraut der Gegenseite, umgekehrt aber nicht (bzw. es ist noch unbekannt, es wird auf eine Antwort gewartet)
  • paired: beide Linkpartner vertrauen einander
Zustandsübergänge
EreignisReaktion
TCP-ACK other := null link := null send(DESCRIPTOR_UID, my_uid)
TCP-FIN other := null link := null
receive(DESCRIPTOR_UID, their_uid) other := their_uid if known_trusted(other): link := my-request send(STATUS_PAIRED) else link := unpaired send(STATUS_UNPAIRED)
receive(STATUS_PAIRED) if link == my-request: link := paired store_trust(other)
receive(STATUS_UNPAIRED) if link == paired: remove_trust(other) link := unpaired
receive(PAIRING_REQUEST) if link == paired: send(STATUS_PAIRED) if link == my-request: send(STATUS_PAIRED) if link == unpaired: show_prompt() link := their-request
Anwender akzeptiert Pairing
oder löst es aus
if link == unpaired: link := my-request send(PAIRING_REQUEST) if link == their-request: link := my-request send(PAIRING_REQUEST) send(STATUS_PAIRED)
Anwender (oder Timeout) lehnt Pairing ab link := unpaired send(STATUS_UNPAIRED)

Zustandsdiagramm mit den wichtigsten Übergängen. Gestrichelt dargestellt: Übergang aus beliebigem Zustand.

Ist netBiDiB eine 1:1 oder eine 1:N-Verbindung?

Beide Anwendungsfälle werden durch netBiDiB abgedeckt. Prinzipiell entscheidet

  • ein Client, zu wie vielen Servern er einen Link aufbaut
  • ein Server, von vielen Clients er (gleichzeitig) Links akzeptiert
  • ein Knoten, an welchem Interface er sich anmeldet (maximal eines)
  • ein Interface, wie viele (gleichzeitige) Anmeldungen von Knoten es akzeptiert (maximal 255)

Ein Server soll dabei so viele Links verwalten wie seine Ressourcen es zulassen. Ein Client (Interface, Host) kann entscheiden ob er nur einzelne Verbindungen oder beliebig viele nutzt. Aus Anwendersicht sind jedoch mehrfache Verbindungsmöglichkeiten zu bevorzugen, insbesondere bei der Implementation von Discovery ist dies unumgänglich für das Einlesen der Descriptoren aller Links.

Die Oberfläche eines Hostprogramms, das mehrere Verbindungen nutzen kann, könnte beispielsweise so aussehen:

Wie kann ein Knoten zwischen Hosts wechseln?

An einem physischen Bus lässt sich das Interface, an dem sich der Knoten anmelden soll, einfach anhand der Steckverbindung auswählen und ein Wechsel durch simples Umstöpseln vollziehen. Bei einem rein virtuellen Bus muss das 'Abstecken' jedoch explizit in der Software modelliert werden.

Das Trennen der TCP-Verbindung wäre eine Möglichkeit, verhindert aber eine spätere Wiederanmeldung durch den Knoten. Das Senden von MSG_LOCAL_LINK STATUS_UNPAIRED würde auch funktionieren, impliziert jedoch einen Vertrauensverlust und verhindert eine Neuanmeldung ohne vorausgehendes Pairing.

Daher wird nur die Anmeldung aufgehoben, dies kann sowohl durch den Knoten mit MSG_LOCAL_LOGOFF wie durch das Interface mit MSG_LOCAL_LOGON_REJECTED erfolgen. Der Link bleibt offen (paired) und steht für eine Wiederanmeldung zur Verfügung.

Der abgemeldete Knoten wählt nun automatisch ein anderes Interface aus, mit dem er gepaired ist (d.h. welches verbunden und vertrauenswürdig ist), und versucht sich dort anzumelden. Der Anwender kann so den Knoten einfach zwischen mehreren Programmen, die alle einen Link zum Knoten aufgebaut haben, wechseln lassen indem er jeweils die Verbindung freigibt.

Der Knoten soll diejenigen Interfaces bevorzugen, an denen er zuletzt angemeldet war, sodass ein Hin- und Herwechseln zwischen zwei Programmen durch einen einzigen Knopfdruck möglich ist.

Um einen beliebigen Wechsel zwischen mehr als zwei Programmen zu ermöglichen, wird ein PAIRING_REQUEST als Einladung zur Anmeldung verstanden. So meldet sich beim initialen Pairing der Knoten auch gleich an dem jeweiligen Interface an, ein PAIRING_REQUEST auf einem bereits gepairten Link (der sofort mit STATUS_PAIRED bestätigt wird) gibt dem jeweiligen Interface die höchste Priorität bei der nächsten Anmeldung. Sobald die bestehende Verbindung freigegeben wird, wechselt der Knoten zu dem priorisierten Interface.