Crisix ist ein dezentraler Android-Messenger für Krisensituationen. Wenn das Internet ausfällt, wechselt Crisix automatisch auf alternative Transportwege – Bluetooth, Wi-Fi Direct, SMS, DNS-Tunnel oder LoRa – ohne zentrale Server, ohne SIM-Karte, ohne Internet.
- Philosophie
- Architektur
- Transport-Layer
- Peer-to-Peer-Netzwerk
- Kryptografie & Sicherheit
- Projektstruktur
- Entwicklung & Build
- DNS-Tunnel-Server
- Fehlerbehebung
Crisix folgt dem Offline-First-Prinzip:
"Erst das lokale Netzwerk, dann das Internet, dann kreative Wege."
Die App priorisiert Transportwege nach ihrer Unabhängigkeit:
- Bluetooth – Keine Infrastruktur nötig, Reichweite ~10m
- Wi-Fi Direct – Kein Router nötig, Reichweite ~100m
- SMS – Kein Internet nötig, aber Mobilfunk nötig
- Lokales WLAN – Router nötig, aber kein Internet
- Internet (P2P) – Internet nötig, aber kein Server
- DNS-Tunnel – Internet nötig, funktioniert hinter restriktiven Firewalls
- LoRa – Keine Infrastruktur nötig, Reichweite ~10km (experimentell)
┌─────────────────────────────────────────────────────────────┐
│ Crisix App │
├─────────────────────────────────────────────────────────────┤
│ UI Layer (Jetpack Compose) │
│ ┌───────────┬───────────┬───────────┬───────────────────┐ │
│ │ ChatList │ Contacts │ QR-Scanner│ ConnectionsScreen │ │
│ └───────────┴───────────┴───────────┴───────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ TransportManager (Automatische Transportwahl) │
│ ┌──────────┬──────────┬──────┬────────┬────────┬───────┐ │
│ │Bluetooth │ WiFi-Dir │ SMS │ WLAN │Internet│ DNS- │ │
│ │Transport │ Transport│ Trans│ Trans │ P2P │Tunnel │ │
│ └──────────┴──────────┴──────┴────────┴────────┴───────┘ │
├─────────────────────────────────────────────────────────────┤
│ P2P-Netzwerk (InternetTransport) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Libp2pManager (TCP-Server, Ed25519, Streams) │ │
│ │ PeerDiscovery (DHT + mDNS + NAT-Traversal) │ │
│ │ MainlineDhtNode (Kademlia DHT, BEP 5) │ │
│ │ NatTraversal (STUN, Hole Punching) │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Datenhaltung │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ContactRepository (verschlüsselte Kontaktliste) │ │
│ │ CryptoHelper (Ed25519, AES-GCM, Schlüsselpaare) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Jeder Transport implementiert das Transport-Interface:
interface Transport {
val type: TransportType
val capabilities: TransportCapabilities
suspend fun isAvailable(): Boolean
suspend fun start()
suspend fun stop()
suspend fun send(peerId: String, data: ByteArray): Result<Unit>
fun registerListener(listener: (String, ByteArray) -> Unit)
fun discoverPeers(): Flow<Peer>
fun getStatusDetail(): Pair<Int, String>
}Der TransportManager ist das zentrale Steuerungsmodul. Er:
- Startet alle verfügbaren Transporte parallel
- Wählt automatisch den besten Transport für jede Nachricht
- Überwacht die Transportverfügbarkeit (ConnectivityManager + eigene Prüfung)
- Bietet einen manuellen Override für erzwungene Transportwahl
Transport-Auswahlstrategie:
- Prüfe, ob der gewünschte Transport verfügbar ist
- Falls nicht, wähle den nächstbesten verfügbaren Transport
- Priorität: Bluetooth > Wi-Fi Direct > SMS > WLAN > Internet > DNS-Tunnel > LoRa
- Nutzt TCP-Sockets im lokalen Netzwerk
- mDNS (Multicast DNS) zur automatischen Peer-Erkennung
- UDP-Broadcast als Fallback, wenn mDNS blockiert ist
- Netzwerkscan über das Subnetz (z.B.
192.168.178.1-255) - Port: Dynamisch (OS-Vergabe), Port 54230 als Standard
Protokoll: TCP mit Längenpräfix (4 Bytes Länge + Daten)
Siehe Peer-to-Peer-Netzwerk.
- Nutzt Android Bluetooth Sockets (RFCOMM)
- Automatische Kopplung über Bluetooth-Discovery
- Verschlüsselung auf App-Ebene (nicht nur Bluetooth-eigen)
- Nutzt Android
SmsManagerAPI - Nachrichten werden Base64-kodiert und in SMS aufgeteilt
- Maximal 160 Zeichen pro SMS-Teil (GSM 7-Bit)
- Automatische Fragmentierung und Reassemblierung
- DNS-Tunnel für Umgebungen, die nur DNS-Verkehr erlauben (Captive Portals, Firewalls)
- Kodiert Nachrichten als DNS-Anfragen an
crisix-dns.onrender.com - Der Server dekodiert die Anfragen und leitet sie weiter
- Funktioniert auch hinter strikten Firewalls
- Nutzt Android
HardwarePropertiesManagerfür LoRa-Hardware (falls vorhanden) - Extrem niedrige Bandbreite (~300 Bit/s)
- Nur für Textnachrichten geeignet
Das Herzstück von Crisix ist das serverlose P2P-Netzwerk über das Internet.
Jeder Peer hat ein Ed25519-Schlüsselpaar:
- Privater Schlüssel (64 Bytes): Wird lokal gespeichert (SharedPreferences)
- Öffentlicher Schlüssel (32 Bytes): Dient als Identität
- Peer-ID / Fingerprint: SHA-256 des öffentlichen Schlüssels (64 Hex-Zeichen)
// Generierung
val keyPair = CryptoHelper.generateKeyPair()
val fingerprint = CryptoHelper.publicKeyToFingerprint(keyPair.publicKey)
// → "abdfd19333dba2f05d29de7afc16819ada3bec6130134b86270726e3387af710"Der Libp2pManager ist ein Singleton, der:
- Einen TCP-Server auf einem dynamischen Port startet
- Eingehende Verbindungen akzeptiert und den Handshake durchführt
- Ausgehende Verbindungen zu Peers herstellt
- Streams für die bidirektionale Kommunikation verwaltet
Handshake-Protokoll:
- Beide Seiten senden SOFORT ihre Peer-ID (2 Bytes Länge + UTF-8 String)
- Gleichzeitiges Senden vermeidet Race Conditions
- Nach dem Handshake: Nachrichtenaustausch mit Längenpräfix (4 Bytes + Daten)
Kombiniert drei Verfahren:
- Eigener DHT-Knoten auf Port 6881 (wie BitTorrent)
- Bootstrap über öffentliche DHT-Seeds:
router.bittorrent.com,dht.transmissionbt.com,router.utorrent.com - Globales Topic (
DhtConfig.GLOBAL_TOPIC): Alle Crisix-Geräte registrieren sich im selben Topic - Announce: Peer-ID + IP/Port werden im Topic gespeichert
- Lookup: Peers können über ihre Peer-ID gefunden werden
// Registrierung in der DHT
dhtNode.announce(
topicBytes = DhtConfig.GLOBAL_TOPIC,
peerId = localPeerId,
publicHost = publicIp, // via STUN ermittelt
publicPort = publicPort
)
// Peer-Suche
val peerInfo = dhtNode.findPeer(targetPeerId)- Multicast DNS für Geräte im selben lokalen Netzwerk
- Dienstname:
_crisix._tcp.local - Keine Konfiguration erforderlich
- Funktioniert auch ohne Internet
- STUN: Ermittelt die öffentliche IP/Port
- UDP Hole Punching: Öffnet Löcher in NATs für direkte Verbindungen
- Fallback: Wenn Hole Punching fehlschlägt, wird die Verbindung über die DHT vermittelt
Implementiert einen Kademlia DHT-Knoten nach BEP 5 (BitTorrent-Protokoll):
- Routing-Tabelle: Verwaltet bekannte Knoten (max. 200)
- Find Node: Sucht nach Knoten in der DHT
- Announce Peer: Registriert einen Peer in einem Topic
- Find Peer: Sucht nach Peers in einem Topic
- Node-ID: SHA-1 der IP (wie BitTorrent)
- Ed25519 für digitale Signaturen und Identität
- Schlüsselpaar wird beim ersten Start generiert
- Persistenz in
SharedPreferences(Base64-kodiert) - Einheitliche ID: Die Peer-ID ist IMMER der Fingerprint des Public Keys
- AES-256-GCM für Nachrichteninhalte
- Ephemeral Key Exchange (X25519) für jede Sitzung
- Perfect Forward Secrecy durch temporäre Sitzungsschlüssel
data class CrisixMessage(
val messageId: String, // UUID
val senderId: String, // Fingerprint des Senders
val recipientId: String, // Fingerprint des Empfängers
val type: MessageType, // CHAT_MESSAGE, ACK, FILE, etc.
val payload: ByteArray, // Verschlüsselter Inhalt
val timestamp: Long // Unix-Zeitstempel
)app/src/main/java/com/messenger/crisix/
├── MainActivity.kt # Einstiegspunkt, EdgeToEdge, Kamera-Berechtigung
├── CrisixApp.kt # (in ui/navigation/) Haupt-App-Komponente
│
├── data/
│ ├── Contact.kt # Datenklasse für Kontakte
│ └── ContactRepository.kt # Persistenz (SharedPreferences + JSON)
│
├── transport/
│ ├── Transport.kt # Transport-Interface
│ ├── TransportManager.kt # Zentrale Transportsteuerung
│ ├── TransportType.kt # Enum: BLUETOOTH, WIFI_DIRECT, SMS, WIFI, INTERNET, DNS_TUNNEL, LORA
│ ├── TransportCapabilities.kt # Fähigkeiten eines Transports
│ ├── Peer.kt # Datenklasse für Peers
│ │
│ ├── WifiTransport.kt # Lokaler WLAN-Transport (TCP + mDNS)
│ ├── BluetoothTransport.kt # Bluetooth-Transport (RFCOMM)
│ ├── SmsTransport.kt # SMS-Transport
│ ├── DnsTunnelTransport.kt # DNS-Tunnel-Transport
│ ├── LoRaTransport.kt # LoRa-Transport (experimentell)
│ │
│ └── internet/
│ ├── InternetTransport.kt # P2P-Transport über Internet
│ ├── Libp2pManager.kt # TCP-Server, Streams, Handshake
│ ├── PeerDiscovery.kt # DHT + mDNS + NAT-Traversal
│ ├── MainlineDhtNode.kt # Kademlia DHT (BEP 5)
│ ├── MainlineDhtClient.kt # DHT-Client für Lookups
│ ├── DhtNode.kt # Abstrakter DHT-Knoten
│ ├── DhtConfig.kt # DHT-Konfiguration (Topics, Ports)
│ ├── BootstrapNodes.kt # Öffentliche DHT-Seeds
│ ├── NatTraversal.kt # STUN + Hole Punching
│ ├── CryptoHelper.kt # Ed25519, AES-GCM, Schlüssel
│ └── CrisixProtocol.kt # Nachrichtenformat, Serialisierung
│
└── ui/
├── navigation/
│ ├── NavRoutes.kt # Routen-Definitionen
│ └── CrisixApp.kt # Navigation, QR-Verarbeitung
│
└── screens/
├── ChatListScreen.kt # Chat-Übersicht
├── ContactListScreen.kt # Kontaktliste
├── ContactDetailScreen.kt # Kontaktdetails + Chat
├── MyIdScreen.kt # Eigene ID (QR-Code anzeigen)
├── QrCodeScannerScreen.kt # QR-Code-Scanner (CameraX + ML Kit)
└── ConnectionsScreen.kt # Verbindungsstatus
- Android Studio Ladybug (2024.2.1+) oder neuer
- JDK 17+
- Android SDK 36 (Android 16)
- Kotlin 2.0+
- Gradle 8.9+
# Debug-Build
./gradlew assembleDebug
# Release-Build
./gradlew assembleRelease
# APK installieren
./gradlew installDebug// build.gradle.kts (app)
dependencies {
// Jetpack Compose (UI)
implementation("androidx.compose.ui:ui:1.7.0")
implementation("androidx.navigation:navigation-compose:2.8.0")
// CameraX (QR-Scanner)
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-mlkit-vision:1.4.0")
// ML Kit (Barcode-Erkennung)
implementation("com.google.mlkit:barcode-scanning:17.3.0")
// Kotlinx Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />Der DNS-Tunnel-Server läuft auf Render.com und ist ein separater Dienst:
dns-tunnel-server/
├── dns_server.py # Python-DNS-Server (Twisted)
├── Dockerfile # Container-Build
├── render.yaml # Render.com-Konfiguration
├── requirements.txt # Python-Abhängigkeiten
└── RENDER_DEPLOY.md # Deployment-Anleitung
Funktionsweise:
- Client kodiert Nachricht als Subdomain:
[base64].crisix-dns.onrender.com - DNS-Anfrage wird an den Server gesendet
- Server dekodiert die Subdomain und extrahiert die Nachricht
- Server leitet die Nachricht an den Ziel-Peer weiter (via P2P)
- Antwort wird als DNS-Antwort (TXT-Record) zurückgesendet
Ursache: Der Peer hat sich noch nicht in der DHT registriert, oder die DHT-Suche war zu früh.
Lösung:
- Warte 30-60 Sekunden nach dem App-Start (DHT-Bootstrap dauert)
- Stelle sicher, dass Internetverbindung besteht
- Prüfe die Logs auf
✅ Mainline-DHT-Knoten gestartet
Ursache: Die Geräte sind in verschiedenen Subnetzen oder Firewalls blockieren den Port.
Lösung:
- Stelle sicher, dass beide Geräte im selben WLAN sind
- Prüfe, ob die WLAN-Client-Isolation im Router deaktiviert ist
- Verwende den QR-Code zum Austausch der IP/Port
Ursache: Android beschränkt Multicast-Sockets ab Android 14.
Lösung:
- Der
WifiTransporthat einen UDP-Broadcast-Fallback - Verwende den QR-Code zur manuellen Verbindung
- Die DHT-Suche funktioniert trotzdem (via Internet)
Ursache: Der Peer hat die Verbindung geöffnet, aber keine Daten gesendet.
Lösung:
- Prüfe, ob beide Geräte die gleiche Protokollversion verwenden
- Der
readFully()-Timeout beträgt 5 Sekunden - Bei wiederholten Timeouts: App neustarten
# Logs filtern
adb logcat -s InternetTransport
adb logcat -s Libp2pManager
adb logcat -s PeerDiscovery
adb logcat -s MainlineDhtNode
adb logcat -s WifiTransport
adb logcat -s TransportManager
# Alle Crisix-Logs
adb logcat | grep "com.messenger.crisix"MIT License – siehe LICENSE.
- libp2p – Inspiration für das P2P-Protokoll
- Mainline DHT – Dezentrale Peer-Findung (BEP 5)
- BitTorrent – Bootstrap-Knoten für die DHT
- Jetpack Compose – Modernes Android-UI-Toolkit
- CameraX + ML Kit – QR-Code-Scanner