BLE Notfall-Mesh ← Startseite / Home

Technische Übersicht des Protokolls

BLE Notfall-Mesh · Kompaktfassung · English version below

Dieses Dokument beschreibt in Grundzügen, wie BLE Notfall-Mesh technisch arbeitet — die Prinzipien, nicht die Bauanleitung. Die Sicherheit der App beruht bewusst nicht auf Geheimhaltung des Verfahrens, sondern auf etablierter, öffentlich geprüfter Kryptografie.

Grundidee

Es gibt keinen Server. Jedes Gerät ist gleichzeitig Sender, Empfänger und Brücke. Nachrichten springen über Bluetooth Low Energy (BLE) von Gerät zu Gerät, bis sie ihr Ziel erreichen. Ein Gerät, das gerade keinen Nachbarn hat, hält Pakete vor und gibt sie beim nächsten Treffen weiter. So entsteht Reichweite aus vielen kurzen Sprüngen.

Kryptografische Bausteine

Es kommen ausschließlich standardisierte, breit analysierte Verfahren zum Einsatz:

BausteinVerfahrenWofür
SignaturenEd25519Beweist, von wem eine Nachricht stammt (Echtheit & Unverfälschtheit)
SchlüsseltauschX25519Gemeinsamer Schlüssel für Direktnachrichten, pro Nachricht frisch
VerschlüsselungChaCha20-Poly1305Inhalt verschlüsseln und gegen Veränderung sichern (AEAD)
HashBLAKE2bFingerabdrücke, Paket-IDs, Kanalkennungen
Zugangstoken → SchlüsselHKDF-SHA256Kanalschlüssel aus dem Zugangstoken
Passwort → SchlüsselArgon2idKanalschlüssel mit zusätzlichem Passwort (Token als Salz); Schutz verschlüsselter Sicherungen
SchlüsselableitungHKDF (SHA-256)Sitzungs- und Nachrichtenschlüssel ableiten; saubere Domänentrennung
RuheverschlüsselungAES-256-GCM (Android-Schlüsselspeicher)Sensible Felder auf dem Gerät verschlüsseln: Identität, Kanalschlüssel und -passwörter, Nachrichtentexte, Kontaktnotizen

Jede Identität besteht aus zwei Schlüsselpaaren (Ed25519 zum Signieren, X25519 zum Verschlüsseln). Sie entstehen beim ersten Start auf dem Gerät, werden zusätzlich im Android-Schlüsselspeicher gesichert und verlassen das Gerät nie. Ihr Fingerabdruck (die „Sicherheitsnummer") lässt sich mit einem Gegenüber vergleichen, um einen Kontakt zweifelsfrei zu bestätigen.

Auf dem Gerät gespeichert (Ruheverschlüsselung)

Die empfindlichsten Daten liegen auf dem Gerät nicht im Klartext: die privaten Identitätsschlüssel, die Kanalschlüssel samt Zugangscodes, die Nachrichtentexte sowie deine Kontaktnotizen und Feldwerte werden mit einem gerätegebundenen Schlüssel aus dem Android-Schlüsselspeicher feldverschlüsselt. Eine erbeutete Datenbankdatei gibt daraus weder Inhalte noch Schlüssel preis. Metadaten (Zeitstempel, Paket-IDs, Absenderkennung) bleiben offen, und die Datenbank selbst bleibt entsperrt, damit ein gesperrtes Gerät weiter weiterleitet. Gegen ein laufendes, entsperrtes Gerät hilft das nicht — dafür gibt es App-Sperre, optionale Auto-Löschung nach mehreren Fehlversuchen und Panik-Löschung.

Zwei Wege: Direktnachricht und Kanal

Direktnachricht („Sealed Sender")

In einem Netz, in dem jedes Gerät jedes Paket weiterträgt, darf nirgends im Klartext stehen, für wen ein Paket ist. Deshalb:

Ein weiterleitendes Gerät sieht dadurch weder Inhalt noch Absender noch Empfänger — nur ein verschlüsseltes Bündel, das es blind weiterträgt.

Kanäle

Ein Kanal ist durch ein Zugangstoken definiert: 32 zufällige Byte, die beim Anlegen gewürfelt werden. Aus ihm leitet jedes Gerät per HKDF-SHA256 denselben Kanalschlüssel ab; auf dem Gerät wird nichts registriert und niemand verwaltet Mitglieder. Wer das Token hat, kann mitlesen und schreiben.

Der Kanalname geht in die Ableitung nicht ein — er ist eine reine Beschriftung. Zwei gleichnamige Kanäle sind deshalb zwei verschiedene Kanäle, und ein Kanal bleibt derselbe, auch wenn ihn jemand für sich umbenennt. Auch die Chat-Kennung entsteht aus dem Token (BLAKE2b(token, 8)), nicht aus dem Namen.

Optional kommt ein Passwort hinzu. Dann entsteht der Schlüssel per Argon2id aus Passwort und Token als Salz. Gespeichert wird nur der abgeleitete Schlüssel, nie das Passwort selbst.

Einladungen haben die Form nfmesh-ch:2:<token>:<name>:<flags> und werden als Text, als QR-Code oder als QR-Code in einem Bild weitergegeben — es gibt keine Instanz, bei der man sie hinterlegen könnte. Ein etwaiges Passwort steht ausdrücklich nicht darin; ein Flag sagt nur, dass eines verlangt wird. Wer den Code abfotografiert, kommt damit allein also nicht hinein. Zwei weitere Flags tragen die Rechte, die der Ersteller vergeben hat: ob andere die Reichweite und ob sie die Selbstlöschung für alle ändern dürfen. Das ist eine Absprache unter mitspielenden Geräten, keine Sperre — eine veränderte App könnte sie übergehen. Wird eines von beiden geändert, reist die neue Einstellung als eigene Nachrichtenart mit („alt>neu") und steht bei allen als Zeile im Verlauf, mit Namen: Eine Umstellung, die still im Hintergrund wirkt, bemerkt man sonst erst daran, dass Geschriebenes verschwindet.

Ein Kanal kann ausdrücklich offen angelegt werden. Dann ist das Token nicht zufällig, sondern BLAKE2b("nfmesh-open-v2:" + Name, 32) — jeder mit dieser App kommt über den Namen hinein. Gedacht für Treffpunkte, die man ausrufen und nicht verschicken kann, und ausdrücklich nicht vertraulich: kein Passwort, keine abgesprochene Reichweite, kein „Wer ist da?", keine Beitrittsmeldung. Der eingebaute Notruf-Kanal ist so ein offener Kanal mit festem Token, damit sich Fremde im Ernstfall ohne Vorabsprache erreichen können.

Beitritt, Verlassen und Einladen melden sich über MessageKind.MEMBER = 16 (Rumpf j, l oder i:<Name>) und erscheinen als Zeile im Verlauf. Verschickt jemand eine Einladung, steht dort „X hat Y eingeladen“ — die Runde erfährt vorher, wer gleich mitliest, und auf wessen Zutun. Hinauswerfen kann ohnehin niemand jemanden; um so mehr zählt, dass es alle überhaupt erfahren. Das ist eine Höflichkeit, keine Mitgliederliste: Die Meldung schickt das Gerät von sich aus, eine veränderte App schickt sie nicht, und ein stiller Mitleser mit dem Token taucht nirgends auf. Abschaltbar, und in offenen Kanälen unterbleibt sie — dort wäre sie eine Anwesenheitsmeldung an Fremde.

Neuverschlüsseln vergibt ein neues Passwort und wahlweise ein neues Token; die alte Einladung ist damit wertlos. Die Chat-Kennung bleibt trotzdem die alte — sie ist eine rein lokale Nummer, Pakete finden ihren Kanal über die 4-Byte-Kanal-ID aus dem Schlüssel. So bleibt der Verlauf erhalten, statt einen zweiten Chat aufzumachen.

Funkschicht: Werben, Suchen, Rollen

Jedes Gerät ist gleichzeitig Central (sucht und verbindet) und Peripheral (wirbt und nimmt Verbindungen an). Ohne beides gäbe es keine Begegnung zwischen zwei Nutzern, die beide nur suchen oder beide nur werben. Gesucht wird im Takt — acht Sekunden suchen, zwölf Sekunden Pause; dauerhaftes Suchen wäre der größte Akkufresser der App.

Das Erkennungszeichen im Werbepaket. Neben einer zufälligen Gerätekennung trägt das Signal sechs weitere Byte: eine Zufallszahl und einen Abdruck daraus, gebildet mit einem Schlüssel, den nur die eigenen Kontakte kennen. Wer jemanden im Adressbuch hat, erkennt sein Signal daran bevor eine Verbindung steht; für alle anderen ist es Rauschen, das alle zehn Minuten wechselt und kein Merkmal ergibt, an dem sich ein Gerät über die Zeit wiedererkennen ließe.

Das ist kein Beiwerk, sondern die Bedingung dafür, dass ein Anruf im Gedränge sein Ziel findet: Ohne das Zeichen müsste die App der Reihe nach jeden Nachbarn anwählen und nachsehen, wer es ist — bei zwanzig Geräten ist das Gespräch vorbei, bevor die Hälfte durch ist. Dieselbe Zuordnung trägt die Empfangsanzeige beim Prüfen einer aufgestellten Brücke.

Wer ruft an? Verbinden sich beide Seiten gleichzeitig, entstehen zwei Verbindungen zum selben Gerät, von denen eine sofort wieder gekappt wird — verschwendete Funkzeit und einer von nur drei Verbindungsplätzen. Deshalb wählt nur die Seite mit der größeren Knotenkennung an; die andere wartet. Meldet sich dort 30 Sekunden lang niemand, übernimmt sie doch selbst — sonst bliebe ein Gerät unerreichbar, das nicht anwählen kann.

Weite Reichweite (optional). Zusätzlich zur gewöhnlichen Werbung kann ein Gerät über LE Coded PHY werben und suchen (Bluetooth 5, „Long Range"): Jedes Bit achtfach gespreizt, rund 12 dB mehr Link-Budget, in der Praxis meist die zwei- bis dreifache Entfernung. Ausdrücklich zusätzlich und nicht stattdessen: Ein Gerät, das nur klassisch scannt — und das sind alle vor Bluetooth 5 und viele danach — sieht eine Coded-PHY-Werbung überhaupt nicht. Wer nur noch so würbe, verschwände für die halbe Nachbarschaft. Der Preis sind Akku (zwei Werbesätze, zwei Suchläufe) und achtfache Funkzeit je Paket auf der weiten Strecke; deshalb ist es abschaltbar und standardmäßig aus.

Fluten und Vorhalten (Store-and-Forward)

Jedes Gerät bietet Nachbarn eine Liste der Paket-Kennungen an, die es hat; der Nachbar fordert nur an, was ihm fehlt. Jede Paket-ID ist der Hash des Pakets selbst (inhaltsadressiert), sodass dasselbe Paket nie doppelt gespeichert oder endlos im Kreis geschickt wird. Fremde, unlesbare Pakete werden eine begrenzte Zeit vorgehalten und dann verworfen, ebenso bei vollem Speicher.

Wie lange, hängt von der Nachricht ab. Für unbegrenzte Nachrichten gilt die am Gerät eingestellte Vorhaltezeit (2–72 Stunden, voreingestellt 12). Reichweitenbegrenzte laufen früher ab: eine halbe Stunde bei bis zu 3 Sprüngen, zwei Stunden bei bis zu 10, sechs Stunden darüber. Maßgeblich ist immer die kürzere der beiden Fristen — eine auf drei Sprünge begrenzte Nachricht verschwindet also nach einer halben Stunde aus dem Vorrat, auch wenn das Gerät auf 72 Stunden steht. Notrufe bleiben am längsten: Für sie gilt die doppelte Vorhaltezeit, und reichweitenbegrenzt gesendet werden sie nie.

Prüfzahl eines Kanals

Ein Kanal hat keinen Ort und keine Mitgliederliste — es gibt also nichts, woran sich ablesen ließe, ob zwei Geräte denselben meinen. Der Anzeigename taugt nicht dazu: Er ist bloß eine Beschriftung.

Deshalb zeigt die Einladung die Kanalkennung als Prüfzahl an — dieselben vier Byte, mit denen jedes Paket dieses Kanals im Funkverkehr adressiert ist. Sie stammt aus dem Kanalschlüssel und damit aus Token und Passwort. Gleiche Zahl heißt derselbe Kanal; verschiedene Zahlen heißen zwei getrennte Kanäle, auch wenn beide gleich heißen. Geheim ist daran nichts: Diese Bytes stehen ohnehin unverschlüsselt in jedem Paket.

Wer als Nachbar zählt

Die Startseite zeigt, wie viele Geräte gerade in Reichweite sind. „Gerade“ heißt: in den letzten zwei Minuten gehört. Geräte in Reichweite melden sich etwa im Minutentakt — eine ausgelassene Runde darf niemanden verschwinden lassen, zwei sollen es.

Die Verbindungsschicht merkt sich Gegenstellen deutlich länger — sie braucht das, um eine Verbindung nicht bei jeder Funklücke neu aufzubauen. Die Anzeige folgt bewusst der kürzeren Regel: Sie soll beantworten, wer jetzt erreichbar ist. Die Zeile „Letzter Kontakt vor …“ blickt dagegen weiter zurück, denn sie sagt ausdrücklich etwas über die Vergangenheit.

Gespräche

Ein Gespräch ist im Netz ein Fremdkörper, und deshalb bekommt es eigene Regeln. Nachrichten werden geflutet und vorgehalten: Sie dürfen Umwege nehmen und Stunden später ankommen. Für Sprache ist beides falsch. Anrufpakete tragen deshalb hopLimit = 1, werden nirgends gespeichert und von niemandem weitergetragen. Ein Gespräch geht nur zu einem Gerät in direkter Funkreichweite — wer nicht in Reichweite ist, klingelt nicht.

Wen es angeht, steht im Klingelzeichen. Die Funkadresse eines Knotens ist absichtlich zufällig und gerade nicht aus der Identität abgeleitet — sonst funkte ein Gerät ein Stück seines Fingerabdrucks an jeden Zuhörer. Damit lässt sich vorher nicht sagen, über welche Verbindung der Gemeinte hängt, und das Klingelzeichen geht an alle offenen Nachbarn hinaus. Es trägt deshalb den Adressaten mit: <Anrufer> | <Name> | <Angerufener> — und zwar versiegelt für den Angerufenen, mit denselben Schlüsseln wie eine Direktnachricht. Öffnen kann es nur er; für alle anderen in Reichweite ist es Rauschen, und sie erfahren nicht einmal, dass jemand angerufen wurde. Wer anruft, wird aus dem Siegel errechnet statt aus dem Inhalt gelesen. Sobald das Gegenüber antwortet, steht seine Verbindung fest, und alles Weitere — vor allem der Ton — läuft ausschließlich dort entlang.

„Bei mir klingelt es." Der Angerufene meldet den Eingang des Klingelzeichens sofort zurück und wiederholt das, solange es läutet. Für den Anrufer ist das der einzige Beleg vor dem Abnehmen, dass sein Zeichen tatsächlich angekommen ist — es geht ja an alle offenen Nachbarn hinaus, auch wenn der Gemeinte gar nicht da ist. Es hängt mehr daran als die Anzeige: Ohne Rückmeldung räumt der Anrufer nach kurzer Frist die Funkplätze frei, weil das Ziel dann offenbar an keiner der offenen Verbindungen hängt. Kommt die Meldung, unterbleibt das — sonst kappte genau dieses Räumen die Strecke, über die gerade die Zusage kommen soll. Ältere Fassungen kennen das Zeichen nicht und schicken es nie; dann bleibt es beim bisherigen Verhalten.

Der Ton über WLAN. Beherrscht das Gerät Wi-Fi Aware und ist es erlaubt, nimmt der Ton eine direkte WLAN-Strecke zum anderen Gerät — deutlich mehr Kapazität als Bluetooth, hörbar an der Sprachqualität. Sie führt ausschließlich von Gerät zu Gerät und nie ins Internet: kein Netzzugang, kein Router, keine Anmeldung. Sie entsteht mit dem Gespräch und verschwindet mit ihm; Klingeln, Annehmen und Auflegen laufen weiter über Bluetooth. Kommt sie nicht zustande oder bricht sie weg, trägt der Ton den Weg, den er ohne sie immer gegangen wäre. Deshalb ist sie eine Zugabe und nie eine Voraussetzung — und abschaltbar.

Schlüssel. Abgeleitet wird aus denselben Schlüsseln wie bei Direktnachrichten, aus dem QR-Code des Kontakts. Jedes Gespräch bekommt daraus einen eigenen Schlüssel, und Hin- und Rückrichtung verschiedene: Ein mitgeschnittenes Gespräch hilft beim nächsten nicht. Der Ton läuft mit rund 33 kbit/s (IMA-ADPCM, 8 kHz) und wird nach dem Abspielen verworfen — auf keiner Seite bleibt eine Aufzeichnung zurück.

„Wer ist in Reichweite?“ Vor einem Anruf lässt sich fragen, wer gerade erreichbar ist. Dieselbe Anfrage läuft auch automatisch, sobald ein neuer Nachbar auftaucht — Grundlage für den grünen Punkt am Kontaktbild in Kontaktliste, Chat und Anrufliste, gedrosselt auf höchstens eine Runde pro Minute. Die Frage geht als gewöhnliche, versiegelte Direktnachricht an jeden Kontakt einzeln und mit einer harten Grenze von einem Funkschritt hinaus. Ein offener Rundruf „wer ist da?“ wäre einfacher gewesen und ist bewusst nicht gebaut: Er lüde jeden Zuhörer in Reichweite ein, sich zu erkennen zu geben — genau das, was die zufällige Funkadresse verhindern soll. Eine Antwort über eine Zwischenstation wäre ebenfalls falsch: Sie hieße „erreichbar“ über jemanden, den man nicht anrufen kann.

Anrufliste. Beendete Anrufe werden lokal vermerkt — Name, Richtung, Zeitpunkt, Dauer. Kein Gesprächsinhalt, denn den gibt es nirgends. Die Dauer zählt ab dem Annehmen; ein nie angenommener Anruf steht mit null Sekunden und ist genau daran als verpasst zu erkennen. Einzelne Einträge und die ganze Liste lassen sich löschen, und ein Identitätswechsel ohne Verlaufsübernahme entfernt sie mit.

Verbindungen im Gespräch. Im Normalbetrieb trennt das Netz nach jedem Abgleich, um Akku zu sparen; während eines Anrufs wäre genau das tödlich, deshalb bleiben Verbindungen dann offen. Eine Ausnahme bleibt: Wer zwei Minuten lang gar nichts mehr überträgt, gilt als abgerissen und wird getrennt. Im Gespräch fließt alle 20 ms ein Tonabschnitt und beim Klingeln alle zwei Sekunden ein Zeichen — solche Stille gibt es nur, wenn die Funkstrecke längst weg ist und der Funkchip es noch nicht bemerkt hat.

Begleittext zu Anhängen

Bild, Datei, Sprachnachricht und Standort dürfen einen kurzen Text tragen — im selben Paket. Zwei Pakete könnten getrennte Wege nehmen; ein Text, der zu einem anderen Bild wandert, wäre schlimmer als gar keine Erklärung.

Beim Standort steht der Text davor und der Punkt in der letzten Zeile — dieselbe Anordnung, die ein Notruf schon immer benutzt. Bei den übrigen Anhängen sitzt ein kurzer Umschlag davor: 00 43 | Länge (2 Byte) | Text | Anhang. Ohne Text bleibt der Anhang unverändert, Byte für Byte. Das führende Nullbyte taugt als Erkennungszeichen, weil kein Anhang so beginnt; zur Sicherheit wird zusätzlich geprüft, ob die Längenangabe aufgeht.

Vorratsspeicher leeren

Das Gerät trägt fremde Pakete mit — das ist der Sinn des Netzes. Es bleibt trotzdem das Gerät seines Besitzers: Ein Knopf in den Einstellungen verwirft alle fremden Pakete auf einmal, wahlweise auch die eigenen, noch nicht zugestellten.

Verworfen wird als Grabstein, nicht durch Löschen der Zeile. Verschwände sie, gälte das Paket beim nächsten Inventarabgleich als unbekannt und käme sofort wieder herein. Der Grabstein sagt „kenne ich, will ich nicht": Nutzlast überschrieben, Weitertragen beendet. Was andere Geräte tragen, bleibt unberührt — eine weitergereichte Nachricht lässt sich nicht zurückholen.

Größen und die Notbremse

Ein Paket fasst höchstens 16.384 Byte, davon rund 15.900 Byte Nutzlast — es gibt keine Fragmentierung über mehrere Pakete. Alles, was verschickt wird, muss also in ein einziges Paket passen: Bilder werden auf etwa 640 Pixel Kantenlänge gerechnet und auf 12.000 Byte gedeckelt, Sprachnachrichten auf 10 Sekunden (AMR-NB, 4.750 bit/s), Dateien auf 12.000 Byte. Das ist keine Sparsamkeit um ihrer selbst willen: In einem Flutnetz trägt jedes Gerät in Reichweite jedes Paket mit, und diese Funkzeit brauchen im Ernstfall Hilferufe.

Dazu kommt eine abschaltbare Notbremse („Sparbetrieb“): Sie hält fremde Pakete über 2.000 Byte Nutzlast zurück, solange sie aktiv ist — außer sie sind für dieses Gerät lesbar oder als dringend markiert. Gefiltert wird nach Länge, nicht nach Art, weil ein Relais die Art gar nicht kennen darf: Im Kopf stehen nur Typ, Flags, Hop-Limit, Zeit, Länge und die Paket-ID. Praktisch trennt die Grenze sauber zwischen Text (ein paar hundert Byte) und Anhang (bis 12.000). Ein zurückgehaltenes Paket bleibt im Vorrat liegen und läuft weiter, sobald der Sparbetrieb aus ist.

Wovor das Verfahren schützt

Identität, Wechsel und mehrere Geräte

Deine Identität ist ein Schlüsselpaar, das beim ersten Start auf dem Gerät entsteht und es nie verlässt. Aus ihm entstehen die Sicherheitsnummer zum Abgleich und der Fingerabdruck, mit dem jede Nachricht signiert wird.

Wechseln: Besteht der Verdacht, dass die Schlüssel in fremde Hände geraten sind, erzeugt „Neue Identität“ ein frisches Paar und überschreibt das alte. Kontakte, Kanäle und Einstellungen bleiben; alle Kontakte fallen auf „ungeprüft“ zurück, denn der Abgleich galt der alten Identität. Der Wechsel wirkt nur nach vorn — ohne Forward Secrecy bleibt Mitgeschnittenes mit dem alten Schlüssel lesbar. Und Kanäle hängen an ihrem Zugangstoken, nicht an der Identität.

Funkadresse: Die Kennung, mit der sich zwei Geräte auf der Funkstrecke auseinanderhalten, wird zufällig gewürfelt und bei einem Identitätswechsel neu gezogen. Sie ist nicht aus den Schlüsseln abgeleitet — sonst könnte ein Mitlauscher am Signal erkennen, wessen Gerät dort läuft, ohne je eine Verbindung aufzubauen.

Zwei Geräte, eine Identität: Über eine eingespielte Sicherung möglich, muss aber auf beiden Geräten ausdrücklich eingeschaltet werden. Kanalnachrichten gleichen sich von selbst ab, weil der Kanalschlüssel auf beiden liegt; Direktnachrichten über eine zusätzliche Durchschrift, die auf den eigenen Schlüssel versiegelt ist. Anhänge werden nicht gespiegelt — ein zweiter Weg durchs Netz wäre die Funkzeit nicht wert. Der Preis: Das Schlüsselmaterial liegt doppelt, und ein einzelnes Gerät lässt sich nicht abmelden; wer eines verliert, wechselt die Identität als Ganzes.

Ausblick: mehr Reichweite über LoRa

Eine Idee, kein Versprechen. Nichts davon ist gebaut, und ob es gebaut wird, ist offen.

Bluetooth trägt 20 bis 100 Meter. LoRa — ein schmalbandiger Funk auf 868 MHz — schafft in bebautem Gebiet 2 bis 5 Kilometer und mit freier Sicht deutlich mehr. Ein einziges Gerät auf einem Dach könnte Stadtteile verbinden, die per Bluetooth nie zueinanderfänden.

Architektonisch wäre der Platz vorhanden: Die Funkschicht (MeshLink) ist abstrakt, die beiden Bluetooth-Rollen sind zwei Ausprägungen davon, Zerlegen und Zusammensetzen langer Pakete steckt schon darin. Eine LoRa-Anbindung wäre eine dritte Ausprägung; der Router merkte davon nichts, weil er nur Pakete kennt. Auch die Größenbremse gibt es bereits — eine LoRa-Strecke liefe schlicht dauerhaft im Sparbetrieb.

Die Grenzen sind allerdings hart und nicht verhandelbar:

Falls es je gebaut wird, dann in dieser Reihenfolge: erst ein Versuchsaufbau mit einem Knoten und rohem LoRa, um echte Reichweite und echte Sendezeiten zu messen, dann ein schlankerer Umschlag, und erst danach Bedienung. Alles andere wäre Planung auf Datenblattwerten.

Reichweite ist keine Zusicherung. Ein kleines Hoplimit und die verkürzte Weitertragezeit sind Bitten an fremde Geräte, keine Sperren: Eine veränderte oder ältere Programmfassung hält sich nicht daran, ein Gerät kann sich fortbewegen, und Weiterleiten, Zitieren oder ein Bildschirmfoto entziehen sich ohnehin jeder Technik. Wie viele Menschen eine Nachricht erreicht, wie weit sie kommt und wie lange sie unterwegs ist, ist deshalb nicht garantiert — sie kann von mehr Geräten empfangen, weiter getragen und länger vorgehalten werden als eingestellt. Vertraulichkeit stellt allein die Verschlüsselung her, nicht die Reichweite.

Verworfen: Geohash-Kanäle

Eine geprüfte und bewusst abgelehnte Idee. Hier steht, warum — damit die Begründung nachlesbar ist, wenn die Idee wieder aufkommt.

Der Vorschlag: Kanäle, die sich aus einem geografischen Raster ergeben, und Nachrichten, die einen Radius mitführen — „lokale Warnung, 2 km“ —, den weiterleitende Geräte einhalten. Reizvoll, weil ein Kanal dann von niemandem erstellt werden müsste und eine Meldung nur dort landet, wo sie jemanden angeht.

Warum wir es nicht bauen. Damit ein Relais den Radius einhalten kann, müsste der Herkunftsort unverschlüsselt im Paketkopf stehen — dort, wo heute nur Zeit, Länge, Kennung und Dringlichkeit stehen. Jede Nachricht trüge damit ihren Entstehungsort offen durch die Gegend, mitlesbar für jeden in Reichweite, auch für den, der keinem Kanal beitritt. Und jedes Gerät müsste zum Weiterleiten dauerhaft seine eigene Position kennen: Hintergrundortung auf jedem Telefon, nur damit eine Weiterleitungsentscheidung fallen kann. Beides widerspricht dem Grundsatz, dass der Standort das Gerät nur verlässt, wenn man ihn bewusst sendet.

Dazu kommt: Ein aus dem Raster abgeleiteter Kanalschlüssel ist kein Geheimnis — jeder kann ihn ausrechnen. So ein Kanal wäre der öffentliche Notruf-Kanal mit Extraschritten. Und im Ernstfall ist das Netz dünn; ein Radius kappt genau den Boten, der eine Lücke zu Fuß überbrückt.

Was stattdessen da ist. Die Reichweite pro Chat, gezählt in Sprüngen statt in Kilometern (siehe oben), zusammen mit einer kurzen Weitertragezeit. Das erreicht den nützlichen Teil — Funkzeit sparen — ohne Ortsangabe im Kopf und ohne Ortung im Relais. Wer eine Unterhaltung an einen Ort binden möchte, nennt einen Kanal einfach danach: „Hamburg“ ist ein Thema, kein Gebiet. Wer beitritt, liest mit, wo immer er ist — das ist kein Mangel, sondern dieselbe Ehrlichkeit wie überall sonst: Zugehörigkeit entsteht aus dem Zugangstoken, nicht aus Koordinaten.

Grenzen

Kein System schützt vor allem. Diese Punkte lassen sich technisch nicht vermeiden — deshalb stehen sie hier.

Ortsverzeichnis

Die Suche nach Ortsnamen und Postleitzahlen greift nicht auf die Kartenpakete zu: Deren Beschriftungen liegen verstreut in Hunderttausenden Kacheln, und Postleitzahlen kennt das Kachelschema gar nicht. Stattdessen entsteht aus denselben OpenStreetMap-Daten ein eigenes Verzeichnis — eine SQLite-Datenbank mit 1.225.324 Orten (Städte bis Weiler, mit Punkt und Einwohnerzahl) und 313.662 Postleitzahlen für Deutschland, ganz Europa (ohne Russland und die Türkei) und alle US-Bundesstaaten.

Es liegt in der App, nicht auf einem Server: 102,8 MB, im Paket auf 54,6 MB komprimiert, beim ersten Gebrauch einmal ausgepackt. Damit geht keine Suchanfrage nach außen, und es entsteht kein Protokoll darüber, wonach jemand gesucht hat. Der Preis ist die Paketgröße — bewusst gezahlt, weil eine Suche, die erst noch geladen werden muss, im Krisenfall wertlos wäre.

Gesucht wird nach Wortanfang über einen Index auf name COLLATE NOCASE; bei Mehrdeutigkeit sortiert die App nach Entfernung zur Kartenmitte und Einwohnerzahl. Hausnummern sind nicht enthalten: Allein Deutschland hat über zwanzig Millionen davon.

Eine Eigenheit musste dabei geglättet werden. Die Niederlande vergeben Postleitzahlen je Straßenabschnitt (3514TB) — aus OpenStreetMap kommen 469.352 Stück. Das bläht die Datei und macht die Suche unbrauchbar, weil eine Eingabe Hunderte gleichwertiger Treffer ergäbe. Solche Systeme werden auf ihren Zahlenkern zusammengefasst; übrig bleiben 4.167 Gebiete. Polen mit rund 20.000 echten Zustellzonen bleibt unangetastet.

Offline-Karten

Kartenpakete sind .mbtiles-Dateien — SQLite-Datenbanken mit Kacheln. Verwendet werden Vektorkacheln: Statt fertiger Bilder enthalten sie Geometrien, die auf dem Gerät gezeichnet werden. Das ergibt Straßennamen, die dem Straßenverlauf folgen, saubere Beschriftungsverteilung und bei jeder Zoomstufe scharfe Darstellung.

Gezeichnet wird mit MapLibre. Weil diese Bibliothek Kacheln nur über URLs lädt, läuft in der App ein Server, der ausschließlich an 127.0.0.1 gebunden ist: Er liefert Kacheln aus der lokalen Datei, den Stil und die Schriften. Diese Verbindungen verlassen das Gerät nicht. Die Netzwerkregeln der App verbieten unverschlüsselte Verbindungen grundsätzlich und lassen sie nur für diese Rückschleife zu.

Das Herunterladen eines Kartenpakets ist der einzige echte Netzzugriff der App. Er geschieht nur auf ausdrückliche Aktion des Nutzers, läuft über HTTPS und nimmt nach einem Abbruch per Bereichsanfrage die Übertragung wieder auf. Alternativ importiert man eine Datei, die außerhalb der App geladen wurde.

Erzeugt werden die Pakete mit planetiler aus OpenStreetMap-Daten (ODbL, Kachelschema OpenMapTiles, CC-BY). Enthalten sind Straßen, Straßennamen, Gewässer, Bahnlinien, Ortsnamen und Grenzen bis Zoomstufe 14; Gebäude, Geschäfte und Landbedeckung werden weggelassen, was die Pakete etwa halbiert. Sie müssen im internen App-Speicher liegen: Auf dem FUSE-Speicher unter /sdcard fehlt das Dateisperren, das SQLite zum Öffnen braucht — die App kopiert importierte Pakete deshalb dorthin. Ältere Raster-Pakete bleiben weiterhin lesbar.

Mehrere Karten gleichzeitig

Alle installierten Pakete gelten zusammen — an einer Grenze sieht man beide Seiten, sofern beide Regionen geladen sind. Technisch ist dabei jedes Paket eine eigene Quelle für den Renderer, und die Zeichenvorschrift wird zur Laufzeit entsprechend vervielfacht (aus 18 Ebenen werden bei vier Karten 69).

Der naheliegende Weg — für eine Kachel alle Pakete befragen und die inhaltsreichste nehmen — ist falsch, sobald eine Kachel mehrere Regionen überdeckt, und je weiter man herauszoomt, desto häufiger ist das so. Eine Kachel der Zoomstufe 6 reicht über halb Norddeutschland; dort liefert das Polen-Paket mehr Bytes als das mecklenburgische und gewinnt — zeichnet von Mecklenburg aber nichts. Deshalb wird nicht ausgewählt, sondern übereinandergelegt.

Die Kacheln erreichen den Renderer über einen HTTP-Server, der ausschließlich auf 127.0.0.1 lauscht: Die Kartenbibliothek kann Kacheln nur über URLs laden, nicht direkt aus einer SQLite-Datei. Nach außen geht dabei nichts.

Offen und prüfbar

Bei Fragen zur Technik: info@notfall-mesh.de.


Technical Protocol Overview

BLE Notfall-Mesh · Compact edition

This document outlines how BLE Notfall-Mesh works technically — the principles, not a build manual. The app's security deliberately does not rely on keeping the method secret, but on established, publicly analysed cryptography.

Core idea

There is no server. Every device is sender, receiver and bridge at once. Messages hop from device to device over Bluetooth Low Energy (BLE) until they reach their destination. A device with no neighbour in range holds packets and passes them on at the next encounter. Range is built from many short hops.

Cryptographic building blocks

Only standardised, widely analysed primitives are used:

BlockPrimitivePurpose
SignaturesEd25519Proves who a message is from (authenticity & integrity)
Key exchangeX25519Shared key for direct messages, fresh for each message
EncryptionChaCha20-Poly1305Encrypts content and protects it from tampering (AEAD)
HashBLAKE2bFingerprints, packet IDs, channel identifiers
Access token → keyHKDF-SHA256Channel key from the access token
Password → keyArgon2idChannel key with an additional password (token as salt); protects encrypted backups
Key derivationHKDF (SHA-256)Derives session and per-message keys; clean domain separation
Encryption at restAES-256-GCM (Android keystore)Encrypts sensitive fields on the device: identity, channel keys and passwords, message texts, contact notes

Each identity consists of two key pairs (Ed25519 for signing, X25519 for encryption). They are created on the device at first start, additionally protected by the Android keystore, and never leave the device. Their fingerprint (the “safety number”) can be compared with the other person to confirm a contact beyond doubt.

Stored on the device (encryption at rest)

The most sensitive data does not sit in plaintext on the device: the private identity keys, the channel keys with their access codes, the message texts and your contact notes and field values are field-encrypted with a device-bound key from the Android keystore. A captured database file yields neither contents nor keys. Metadata (timestamps, packet IDs, sender id) stays open, and the database itself stays unlocked so a locked device keeps relaying. This does not help against a running, unlocked device — for that there are the app lock, the optional auto-erase after several wrong attempts, and the panic erase.

Two paths: direct message and channel

Direct message (“sealed sender”)

In a network where every device relays every packet, the recipient must never appear in the clear. Therefore:

A relaying device therefore sees neither content nor sender nor recipient — only an encrypted bundle it carries onward blindly.

Channels

A channel is defined by an access token: 32 random bytes rolled when the channel is created. Every device derives the same channel key from it via HKDF-SHA256; nothing is registered on any device and no one manages members. Anyone who has the token can read and write.

The channel name does not enter the derivation — it is purely a label. Two channels sharing a name are therefore two different channels, and a channel stays the same one even if somebody renames it for themselves. The chat identifier is derived from the token too (BLAKE2b(token, 8)), not from the name.

A password can be added on top. The key is then derived via Argon2id from the password with the token as salt. Only the derived key is stored, never the password itself.

Invitations have the form nfmesh-ch:2:<token>:<name>:<flags> and are passed on as text, as a QR code, or as a QR code inside a picture — there is nowhere to lodge them. Any password is deliberately not part of it; one flag merely says that one is required, so photographing the code is not enough on its own. Two further flags carry the rights the creator granted: whether others may change the range and whether they may change disappearing messages for everyone. That is an agreement between cooperating devices, not a lock — a modified app could ignore it. When either is changed, the new setting travels as its own message kind (“old>new”) and appears for everyone as a line in the history, with the name: a change that works silently in the background is otherwise noticed only when what you wrote starts vanishing.

A channel can be created explicitly as open. Its token is then not random but BLAKE2b("nfmesh-open-v2:" + name, 32) — anyone with this app gets in through the name. Meant for meeting points you have to call out rather than send, and explicitly not confidential: no password, no agreed range, no “Who is here?”, no join announcement. The built-in emergency channel is such an open channel with a fixed token, so strangers can reach each other with no prior arrangement.

Joining, leaving and inviting are announced via MessageKind.MEMBER = 16 (body j or l) and appear as a line in the history. That is a courtesy, not a member list: the device sends it of its own accord, a modified app does not, and a silent reader holding the token appears nowhere. It can be switched off, and it is omitted in open channels — there it would tell strangers someone is present.

Re-keying sets a new password and optionally a new token, making the old invitation worthless. The chat identifier nevertheless stays as it was — it is a purely local number, and packets find their channel through the 4-byte channel id derived from the key. That keeps the history instead of opening a second chat.

Radio layer: advertising, scanning, roles

Every device is central (scanning and connecting) and peripheral (advertising and accepting connections) at once. Without both there would be no encounter between two users who both only scan or both only advertise. Scanning runs in a cycle — eight seconds searching, twelve seconds pause; scanning continuously would be the app's biggest battery drain.

The recognition tag in the advertisement. Besides a random device id, the signal carries six more bytes: a random number and a digest of it, computed with a key only your own contacts hold. Anyone who has you in their address book recognises your signal before a connection exists; to everybody else it is noise that changes every ten minutes and yields no feature by which a device could be recognised again over time.

This is not decoration but the precondition for a call finding its target in a crowd: without the tag the app would have to dial every neighbour in turn to see who it is — with twenty devices the call is over before it is halfway through. The same matching drives the signal reading when you check a bridge you have set up.

Who calls? If both sides connect at the same moment, two links to the same device arise and one is torn down immediately — wasted airtime and one of only three connection slots. So only the side with the larger node id dials; the other waits. If nobody calls there for 30 seconds, it takes over after all — otherwise a device that cannot dial would stay unreachable.

Long range (optional). On top of ordinary advertising, a device can advertise and scan over LE Coded PHY (Bluetooth 5, “long range”): every bit spread eightfold, about 12 dB more link budget, in practice usually two to three times the distance. Deliberately in addition and not instead: a device that only scans the classic way — that is everything before Bluetooth 5 and many after it — cannot see a Coded PHY advertisement at all. Advertising only that way would make you vanish for half the neighbourhood. The price is battery (two advertising sets, two scans) and eight times the airtime per packet on the long link; hence it can be switched off and is off by default.

Flooding and store-and-forward

Each device offers neighbours a list of the packet IDs it holds; the neighbour requests only what it is missing. Every packet ID is the hash of the packet itself (content-addressed), so the same packet is never stored twice or sent around in circles. Foreign, unreadable packets are held for a limited time and then dropped, as they are when storage fills.

How long depends on the message. Unlimited messages follow the retention time set on the device (2–72 hours, 12 by default). Range-limited ones expire sooner: half an hour at up to 3 hops, two hours at up to 10, six hours above that. The shorter of the two always wins — a message limited to three hops therefore leaves the store after half an hour, even if the device is set to 72 hours. Emergency calls are kept the longest: they get double the retention time, and they are never sent range-limited.

A channel’s check number

A channel has no place and no member list — so there is nothing to read off whether two devices mean the same one. The display name will not do: it is merely a label.

The invitation therefore shows the channel id as a check number — the same four bytes that address every packet of this channel on the air. It derives from the channel key and thus from token and password. The same number means the same channel; different numbers mean two separate channels, even if both carry the same name. Nothing about it is secret: those bytes travel unencrypted in every packet anyway.

What counts as a neighbour

The home screen shows how many devices are in range right now. “Right now” means: heard from within the last two minutes. Devices in range check in roughly every minute — one missed round must not make anyone vanish, two should.

The link layer remembers peers considerably longer — it needs to, so a connection is not rebuilt at every gap in reception. The display deliberately follows the shorter rule: it answers who is reachable now. The line “last contact …” looks further back, because it explicitly speaks about the past.

Calls

A call is a foreign body in this network, so it gets its own rules. Messages are flooded and held: they may take detours and arrive hours later. For speech both are wrong. Call packets therefore carry hopLimit = 1, are stored nowhere and relayed by nobody. A call only works to a device in direct radio range — whoever is out of range does not ring.

Who it concerns is stated in the ring signal. A node's radio address is deliberately random and specifically not derived from the identity — otherwise a device would broadcast part of its fingerprint to every listener. So there is no way to know in advance which link the intended device is on, and the ring signal goes out to every open neighbour. It therefore carries the addressee with it: <caller> | <name> | <callee>. Only whoever recognises themselves in the third field rings. As soon as the other side answers, their link is known, and everything after that — the audio above all — travels only there.

“It is ringing here.” The callee acknowledges the ring signal at once and keeps repeating that while it rings. For the caller this is the only proof, before anyone picks up, that the signal actually arrived — it goes out to every open neighbour, even when the intended device is not there at all. More depends on it than the display: without an acknowledgement the caller clears the radio slots after a short grace period, because the target evidently is not on any of the open links. With it, that is skipped — otherwise the clearing would cut the very link the answer is about to arrive on. Older versions do not know the signal and never send it; for them the previous behaviour stands.

Audio over Wi-Fi. If the device supports Wi-Fi Aware and it is allowed, the audio takes a direct Wi-Fi link to the other device — far more capacity than Bluetooth, audible in the speech quality. It runs strictly device to device and never to the internet: no network access, no router, no login. It is created with the call and disappears with it; ringing, answering and hanging up keep running over Bluetooth. If it does not come up, or breaks away, the audio takes the path it would always have taken without it. That is why it is an addition and never a precondition — and can be switched off.

Keys. Derived from the same keys as direct messages, from the contact's QR code. Every call gets its own key from them, and separate ones for each direction: a recorded call does not help with the next one. The audio runs at about 33 kbit/s (IMA ADPCM, 8 kHz) and is discarded after playback — no recording remains on either side.

“Who is in range?” Before a call you can ask who is currently reachable. The same request also runs automatically whenever a new neighbour appears — the basis for the green dot on a contact’s picture in the contact list, chat and call log, throttled to at most one round per minute. The question goes out as an ordinary, sealed direct message to each contact individually and with a hard limit of one radio hop. An open broadcast “who is there?” would have been simpler and is deliberately not built: it would invite every listener in range to identify themselves — exactly what the random radio address prevents. A reply via a relay would be wrong too: it would say “reachable” about someone you cannot call.

Call list. Finished calls are noted locally — name, direction, time, duration. No call content, because none exists anywhere. The duration counts from answering; a call never answered stands at zero seconds and is recognisable as missed by exactly that. Single entries and the whole list can be deleted, and an identity change without keeping the history removes it too.

Links during a call. In normal operation the network disconnects after each exchange to save battery; during a call that would be fatal, so links stay open then. One exception remains: anything that transmits nothing at all for two minutes counts as broken off and is disconnected. During a call an audio chunk flows every 20 ms and while ringing a signal every two seconds — such silence only happens when the radio link is long gone and the chip has not noticed yet.

Text alongside an attachment

A picture, file, voice message or location may carry a short text — in the same packet. Two packets could take different paths; a text that wanders to a different picture would be worse than no explanation at all.

For a location the text comes first and the point sits on the last line — the same arrangement an emergency call has always used. For the other attachments a short wrapper precedes the payload: 00 43 | length (2 bytes) | text | attachment. Without a text the attachment stays unchanged, byte for byte. The leading zero byte works as a marker because no attachment starts that way; as a safeguard the length is checked for consistency as well.

Emptying the relay store

A device carries other people's packets — that is the point of the network. It still belongs to its owner: a button in the settings drops every foreign packet at once, optionally the device's own undelivered ones as well.

Dropping happens as a tombstone, not by deleting the row. If the row vanished, the packet would count as unknown at the next inventory sync and return immediately. The tombstone says “I know it, I don't want it”: payload overwritten, relaying stopped. What other devices carry is unaffected — a message already passed on cannot be recalled.

Sizes and the emergency brake

A packet holds at most 16,384 bytes, of which around 15,900 are payload — there is no fragmentation across packets. Everything sent must therefore fit into a single packet: pictures are scaled to about 640 pixels on the long edge and capped at 12,000 bytes, voice messages at 10 seconds (AMR-NB, 4,750 bit/s), files at 12,000 bytes. This is not frugality for its own sake: in a flooding network every device within range carries every packet, and that airtime is needed for calls for help.

On top of that there is a switchable emergency brake (“frugal mode”). While active it holds back foreign packets above 2,000 bytes of payload — unless they are readable by this device or marked urgent. The filter goes by length, not by type, because a relay must not know the type: the header carries only type, flags, hop limit, time, length and the packet id. In practice the limit separates text (a few hundred bytes) from attachments (up to 12,000) cleanly. A held-back packet stays in the store and moves on once frugal mode is switched off.

Place index

Searching for place names and postcodes does not touch the map packages: their labels sit scattered across hundreds of thousands of tiles, and the tile schema has no notion of postcodes. Instead, a separate index is generated from the same OpenStreetMap data — an SQLite database with 1,225,324 places (cities down to hamlets, with position and population) and 313,662 postcodes for Germany, all of Europe (excluding Russia and Turkey) and every US state.

It lives inside the app, not on a server: 102.8 MB, compressed to 54.6 MB in the package, unpacked once on first use. No search request leaves the device, and no record is kept of what anyone searched for. The price is package size — paid deliberately, because a search that first has to be downloaded is worthless in a crisis.

Matching is by word start over an index on name COLLATE NOCASE; where a name is ambiguous the app sorts by distance from the map centre and by population. House numbers are not included: Germany alone has over twenty million of them.

What the design protects

Identity, rotation and multiple devices

Your identity is a key pair created on the device at first start; it never leaves. From it come the safety number used for comparison and the fingerprint that signs every message.

Rotating: if you suspect the keys have fallen into someone else’s hands, “New identity” creates a fresh pair and overwrites the old one. Contacts, channels and settings stay; every contact falls back to “unverified”, because the comparison applied to the old identity. The change works forwards only — without forward secrecy, recorded traffic stays readable with the old key. And channels depend on their access token, not on the identity.

Radio address: the identifier two devices use to tell each other apart on the radio link is drawn at random and drawn again when you change identity. It is not derived from the keys — otherwise a listener could tell whose device is running from the signal alone, without ever opening a connection.

Two devices, one identity: possible via a restored backup, but it has to be switched on explicitly on both devices. Channel messages sync by themselves because the channel key is on both; direct messages through an extra carbon copy sealed to your own key. Attachments are not mirrored — a second trip through the network would not be worth the airtime. The price: the key material exists twice, and a single device cannot be signed out; whoever loses one replaces the identity as a whole.

Outlook: more range via LoRa

An idea, not a promise. None of this is built, and whether it will be is open.

Bluetooth carries 20 to 100 metres. LoRa — a narrowband radio on 868 MHz — manages 2 to 5 kilometres in built-up areas and considerably more with line of sight. The link layer (MeshLink) is abstract and already handles splitting and reassembling long packets, so a LoRa link would simply be a third kind; the router would not notice.

The limits are hard, though. It needs extra hardware (roughly €30–45) since no phone has LoRa. Airtime is capped by law and depends on the sub-band: 1 % on 868.0–868.6 MHz, about 36 seconds per hour, but 10 % at up to 500 mW on the narrow 869.40–869.65 MHz band — some six minutes, and where Meshtastic's European default channel sits (869.525 MHz). At ~1 kbit/s and ~300 bytes per short message that is a good hundred messages per hour and device, divided among every device repeating the same message. Text, location and emergency calls — never pictures, voice or files. A LoRa signal is direction-findable over kilometres, unlike a 30-metre Bluetooth beacon. And the firmware need not be ours: Meshtastic devices talk to a phone over a documented Bluetooth interface, and our packets are already encrypted — they could travel as opaque payload, leaving the board as nothing but an antenna.

Reach is not a guarantee. A small hop limit and the shortened carry time are requests to other people’s devices, not barriers: a modified or older version of the software does not honour them, a device can move, and forwarding, quoting or a screenshot escape any technical measure anyway. How many people a message reaches, how far it travels and how long it stays in transit is therefore not guaranteed — it may be received by more devices, carried further and held longer than set. Confidentiality comes from the encryption alone, not from the reach.

Rejected: geohash channels

An idea we examined and deliberately turned down. The reasoning is written down here so it can be looked up whenever the idea comes up again.

The proposal: channels derived from a geographic grid, and messages carrying a radius — “local warning, 2 km” — that relaying devices honour. Appealing, because nobody would have to create the channel and a notice would only land where it concerns someone.

Why we are not building it. For a relay to honour a radius, the place of origin would have to sit unencrypted in the packet header — where today only time, length, id and urgency live. Every message would then carry its origin openly, readable by anyone in range, including someone who joins no channel. And every device would have to know its own position permanently just to relay: background location on every phone, purely to make a forwarding decision. Both contradict the principle that your location leaves the device only when you deliberately send it.

On top of that: a channel key derived from a grid is no secret — anyone can compute it. Such a channel would be the public emergency channel with extra steps. And in an emergency the network is sparse; a radius cuts off exactly the courier who bridges a gap on foot.

What exists instead. The reach setting per chat, counted in hops rather than kilometres (see above), together with a short carry time. That achieves the useful part — saving airtime — without a location in the header and without positioning in the relay. If you want to tie a conversation to a place, simply name a channel after it: “Hamburg” is a topic, not a territory. Whoever joins reads along wherever they are — not a shortcoming but the same honesty as everywhere else: membership comes from the access token, not from coordinates.

Limits

No system protects against everything. These points cannot be avoided technically — which is why they are listed here.

Offline maps

Map packages are .mbtiles files — SQLite databases holding tiles. They contain vector tiles: instead of ready-made images they hold geometry that is drawn on the device. This yields street names following the course of the road, proper label placement and a sharp picture at every zoom level.

Rendering is done by MapLibre. Since that library only loads tiles via URLs, the app runs a server bound exclusively to 127.0.0.1: it serves tiles from the local file, the style and the fonts. These connections never leave the device. The app's network rules forbid cleartext connections in general and permit them only for this loopback.

Downloading a map package is the app's only real network access. It happens on explicit user action, over HTTPS, and resumes via range requests after an interruption. Alternatively a file fetched outside the app can be imported.

Packages are produced with planetiler from OpenStreetMap data (ODbL, OpenMapTiles schema, CC-BY). They contain roads, street names, water, railways, place names and boundaries up to zoom level 14; buildings, shops and land cover are omitted, which roughly halves their size. They must live in internal app storage: the FUSE storage under /sdcard lacks the file locking SQLite needs — which is why the app copies imported packages there. Older raster packages remain readable.

Several maps at once

All installed packages apply together — at a border you see both sides, provided both regions are loaded. Technically each package is its own source for the renderer, and the style is multiplied accordingly at runtime (18 layers become 69 with four maps).

The obvious approach — query every package for a tile and take the richest — is wrong as soon as a tile spans several regions, and the further you zoom out the more often it does. A zoom-6 tile covers half of northern Germany; there the Poland package yields more bytes than the Mecklenburg one and wins — while containing nothing of Mecklenburg. So the packages are layered, not selected.

Open and auditable

Questions about the technology: info@notfall-mesh.de.