Files
rustification-server/docs/realtime/research/01-webrtc-tauri-mobile.md
MDietermann 55a2af4756 docs(realtime): record the design decisions behind the realtime layer
Eleven resolved decision tickets plus two research notes, produced with
/wayfinder before any code was written. They live next to the code they explain
rather than in the issue tracker: they are reasoning documents, not open work,
and in a tracker they would drift from the code with no diff and no review.

The value is in the rejected alternatives. Why a counter under a row lock and
not BIGSERIAL, why an event log and not a column on messages, why resync is a
200 with a discriminated body and not a 4xx, why the fan-out trait was deleted
again, why voice is peer-to-peer with a hard six-person cap and no speculative
SFU seam, and why Linux needs a native media stack while the other three
platforms do not.

Two entries record where the spec was wrong and got corrected: created_at was
never a safe sort key, and an unread count cannot be derived without fetching
because seq counts events rather than messages.

Open work is tracked as issues, not here.
2026-08-14 19:04:41 +02:00

45 KiB
Raw Blame History

Recherche-Ergebnis: WebRTC im Tauri-WebView (Mobile + Desktop)

Ticket: ../issues/01-research-webrtc-tauri-mobile.md Map: ../map.md Stand: 14.08.2026 Methodik: Primärquellen (Apple/WebKit, AOSP, Chromium, Microsoft Learn, W3C/IETF, wry-/Tauri-Quellcode auf den tatsächlich aufgelösten Versions-Tags). Blogposts nur als Hinweisgeber, jeweils mit Datum markiert.


1. Kernantwort

Plattform Antwort Aufwand Blockierender Punkt
iOS (WKWebView) Ja 1 Datei NSMicrophoneUsageDescription in src-tauri/Info.ios.plist
Android (System WebView) Ja 2 Manifest-Zeilen RECORD_AUDIO und MODIFY_AUDIO_SETTINGS
Windows (WebView2) Ja keiner — (funktioniert out of the box, mit WebView2-eigenem Prompt)
Linux (WebKitGTK 4.1) Nein hoch / teils unlösbar Drei unabhängige Gates, siehe §5

Fazit für die Map: Die im Council-Verdikt als Risiko 2 geführte „Mobile-WebRTC-Unbekannte" ist aufgelöst — positiv. Android und iOS tragen WebRTC im WebView. Das Voice-Signaling-Design ist für die Mobile-Hälfte der Zielplattformen nicht wertlos. Ticket 04 (Voice-Topologie) ist damit entblockt.

Die neue schlechte Nachricht liegt woanders: Nicht Mobile ist der Problemfall, sondern Linux. Dort ist WebRTC in WebKitGTK weder einkompiliert noch zur Laufzeit aktivierbar noch berechtigt. Das ist keine Kleinigkeit für ein Projekt, dessen Zielgruppe Selbsthoster sind.


2. Ausgangslage im Repo (Versionsanker)

Diese Recherche gilt gegen die Versionen, die rustification-client heute tatsächlich auflöst:

Komponente Version Herkunft
@tauri-apps/cli 2.11.4 package-lock.json
tauri (Rust) "2" → aktuell 2.11.5 (01.07.2026) src-tauri/Cargo.toml, crates.io
tauri-runtime-wry 2.11.4 (30.06.2026) crates.io
→ wry Anforderung ^0.55.0 → 0.55.1 (04.05.2026) crates.io Dependency-Auflösung

Das ist der wichtigste Kontext des ganzen Dokuments: wry 0.56.0 (30.07.2026) hat die Permission-Lage grundlegend geändert, aber tauri-runtime-wry 2.11.4 fordert ^0.55.0 und kann 0.56 nicht ziehen (SemVer bei 0.x: >=0.55.0, <0.56.0). Alles unten Beschriebene ist gegen wry 0.55.1 verifiziert.

Weitere relevante Repo-Fakten:

  • src-tauri/gen/android und src-tauri/gen/apple existieren noch nicht — Mobile ist nie initialisiert worden. Es gibt also noch nichts zu migrieren.
  • app.security.csp ist null. Tauri v2 hat keine Default-CSP (v2.tauri.app/security/csp: „The CSP protection is only enabled if set on the Tauri configuration file."). Für WebRTC ist das irrelevant (siehe §7.5), aber es ist gut zu wissen, dass hier nichts im Weg steht.
  • Es existiert kein WebRTC- oder WebSocket-Code im Client. Grüne Wiese.

3. iOS — Ja

3.1 Versionsuntergrenze

getUserMedia ist in WKWebView (nicht nur Safari) seit iOS 14.3 verfügbar. Primärquelle: WebKit-Blog, MediaRecorder API, Youenn Fablet, 23.11.2020 (webkit.org/blog/11353), wörtlich:

„Speaking of Safari in latest iOS 14.3 beta and local capture, navigator.mediaDevices.getUserMedia can now be exposed to WKWebView applications. navigator.mediaDevices.getUserMedia is automatically exposed if the embedding application is able to natively capture either audio or video."

Der Satzteil „able to natively capture" ist die eigentliche Bedingung: Er bedeutet die Usage-Description-Keys sind in der Info.plist vorhanden. Kein Entitlement, keine private API.

Tauris eigener Default bundle.iOS.minimumSystemVersion ist "14.0" — für WebRTC muss man das faktisch auf 14.3 oder höher lesen. Praktisch relevanter Floor ist ohnehin iOS 15.0, weil erst dort der WKUIDelegate-Callback existiert, den wry registriert (siehe 3.3).

3.2 Secure Context — passt, ohne Tricks

getUserMedia verlangt einen Secure Context. Tauri liefert das Frontend auf iOS (wie macOS und Linux) unter dem Custom-Scheme tauri://localhost aus (crates/tauri/src/manager/mod.rs, tauri_protocol_url). Ein Custom-Scheme allein ist nicht vertrauenswürdig — aber WebKit prüft den Host unabhängig vom Schema (Source/WebCore/page/SecurityOrigin.cpp):

if (LegacySchemeRegistry::shouldTreatURLSchemeAsSecure(protocol)) return true;
if (SecurityOrigin::isLocalHostOrLoopbackIPAddress(host))         return true;

Host ist localhost ⇒ potentially trustworthy ⇒ Secure Context. Der aus Blogposts bekannte _registerURLSchemeAsSecure-Hack (private API) ist nicht nötig.

3.3 Was wry bereits erledigt — verifiziert im Quellcode

wry registriert einen WKUIDelegate und darin den Media-Capture-Callback. Quelle: src/wkwebview/class/wry_web_view_ui_delegate.rs am Tag wry-v0.55.1:

#[unsafe(method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:))]
fn request_media_capture_permission(
  &self, _webview: &WryWebView, _origin: &WKSecurityOrigin, _frame: &WKFrameInfo,
  _capture_type: WKMediaCaptureType,
  decision_handler: &Block<dyn Fn(WKPermissionDecision)>,
) {
  (*decision_handler).call((WKPermissionDecision::Grant,));
}

Drei Beobachtungen, alle wichtig:

  1. Kein #[cfg(target_os = "macos")]. Die Methode wird auf iOS mitkompiliert. (In dev/0.56 steht dort #[cfg(not(macos_12_unavailable))] — ein Flag, das der build.rs nur unter target_os = "macos" setzt; auch dort bleibt iOS also drin.)
  2. In 0.55.1 wird bedingungslos Grant zurückgegeben. Es gibt in dieser Version gar keinen permission_handler — WryWebViewUIDelegateIvars hat das Feld schlicht nicht. Der WebKit-Prompt entfällt damit; es bleibt genau ein Prompt, nämlich der des Betriebssystems (TCC).
  3. Apple dokumentiert den Default ohne Delegate-Implementierung als WKPermissionDecision.prompt — also nicht Deny (Apple Developer, verfügbar ab iOS 15.0 / macOS 12.0). wry macht daraus ein Grant. Sicherheitsrelevant, siehe §7.6.

allowsInlineMediaPlayback setzt wry auf iOS hart auf true — relevant, sobald der Remote-Stream in ein <audio>/<video>-Element gehängt wird.

3.4 Der eine nötige Konfigurationsschritt

src-tauri/Info.ios.plist (oder src-tauri/Info.plist, siehe unten) anlegen:

<key>NSMicrophoneUsageDescription</key>
<string>Rustification braucht Mikrofonzugriff für Sprachanrufe.</string>

Fehlt der Key, gilt die App als nicht „able to natively capture" ⇒ navigator.mediaDevices ist gar nicht erst definiert (das klassische navigator.mediaDevices is undefined), und ein nativer Capture-Versuch bringt iOS zum harten Absturz.

Tauri merged die Datei mit den generierten Werten. Merge-Reihenfolge auf iOS (später gewinnt), laut offenem Issue tauri#13068 (24.03.2025, Unify/Document handling of Info.plist files): XcodeGen-project.yml → Info.plist aus TAURI_DIR → Info.ios.plist aus TAURI_DIR → generierte Version-Strings. Zusätzlich existiert der Config-Key bundle.iOS.infoPlist („Path to an Info.plist file for merging with defaults").

Info.ios.plist ist der robuste Weg, weil er gen/apple wegwerfbar hält.

Belastbares Signal, dass genau dieser Pfad vorgesehen ist: Tauris eigenes API-Beispiel examples/api/src-tauri/Info.plist enthält wörtlich NSMicrophoneUsageDescription = „Request microphone access for WebRTC".

3.5 Bekanntes offenes Issue

tauri#12068 — iOS Microphone Access, geöffnet 28.12.2024, noch offen, Label needs triage, keine Maintainer-Antwort. Symptom: „it failed to access the mic nor enumerate media devices. The mic permission is shown randomly once."

Der Report enthält weder tauri info noch die iOS-Version noch die Info.plist. Wahrscheinlichste Erklärung ist der fehlende NSMicrophoneUsageDescription-Key — das Symptombild passt exakt. Das ist aber eine Hypothese, kein Befund. Siehe Experiment E1 in §8.


4. Android — Ja

4.1 Versionsuntergrenze

WebRTC ist im Android System WebView seit Android 5.0 Lollipop / API 21 / WebView 37 vorhanden. Primärquelle: developer.android.com/about/versions/android-5.0:

„Android 5.0 updates the WebView implementation to Chromium M37 … This release brings support for the following open standards: WebAudio, WebGL, WebRTC." „This release introduces the PermissionRequest class … Your app must have the appropriate Android permissions for these resources in order to grant the permissions to the WebView."

Tauris Default bundle.android.minSdkVersion ist 24 — deutlich darüber. wrys Runtime-Permission-Pfad ist auf SDK_INT >= M (API 23) gegated. Praktisch irrelevant: Das System WebView ist seit Lollipop über den Play Store aktualisierbar, jedes reale Gerät fährt ein aktuelles Chromium.

4.2 Der WebView-Gate: onPermissionRequest

Ohne Override verweigert der WebChromeClient. AOSP, core/java/android/webkit/WebChromeClient.java:

/** ... If this method isn't overridden, the permission is denied. */
public void onPermissionRequest(PermissionRequest request) {
    request.deny();
}

wry überschreibt es. Verifiziert in src/android/kotlin/RustWebChromeClient.kt am Tag wry-v0.55.1:

override fun onPermissionRequest(request: PermissionRequest) {
  val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
  val permissionList: MutableList<String> = ArrayList()
  if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
    permissionList.add(Manifest.permission.CAMERA)
  }
  if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
    permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
    permissionList.add(Manifest.permission.RECORD_AUDIO)
  }
  if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
    ... permissionLauncher.launch(permissions)   // OS-Prompt
        // Callback: alle granted -> request.grant(request.resources), sonst request.deny()
  } else {
    request.grant(request.resources)
  }
}

Die App muss also nichts abfangen. wry löst den Runtime-Prompt selbst aus und reicht das Ergebnis an den WebView durch. Der Callback kollabiert das Ergebnis-Map mit „alle müssen true sein".

Diese Methode existiert bei wry seit 0.21.0 (05.09.2022, PR #685) — sie ist in jedem Tauri-v2-Release vorhanden.

4.3 Die zwei nötigen Manifest-Zeilen

Tauris generiertes Manifest enthält nur INTERNET. Verifiziert im Template crates/tauri-cli/templates/mobile/android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />

In src-tauri/gen/android/app/src/main/AndroidManifest.xml ergänzen:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />

MODIFY_AUDIO_SETTINGS ist nicht optional, obwohl Chromium selbst es nicht braucht. Begründung aus dem Code oben: wry fordert beide Permissions in einem RequestMultiplePermissions-Aufruf an und verlangt, dass alle zurückgemeldeten Werte true sind. Ein nicht im Manifest deklariertes Recht liefert dort false — also request.deny() und NotAllowedError, obwohl der Nutzer den Mikrofon-Dialog mit „Zulassen" bestätigt hat. MODIFY_AUDIO_SETTINGS hat Protection-Level normal, erzeugt also keinen zusätzlichen Dialog; es muss nur deklariert sein.

Das ist die wahrscheinlichste Ursache von tauri#10846 (31.08.2024, Audio Permission Not Working in Tauri Android App Despite Manifest Declaration, needs triage) — dort wurde ausschließlich RECORD_AUDIO deklariert. Deckt sich mit der Community-Diagnose in Discussion #12732 (Feb–Apr 2025, getestet gegen Tauri 2.10.3): „Critically, MODIFY_AUDIO_SETTINGS to prevent NotReadableError." Status: durch Code-Lesung sehr gut gestützt, nicht empirisch verifiziert — siehe Experiment E2.

Es gibt keinen tauri.conf.json-Key für Android-Manifest-Permissions und kein offizielles Tauri-Plugin für generische Runtime-Permissions (die offizielle Plugin-Liste auf v2.tauri.app/plugin führt 30 Plugins, keines davon Mikrofon oder Permissions). Der dokumentierte Weg ist Hand-Editieren des generierten Manifests.

4.4 gen/android gehört ins Git

Das Template-.gitignore schließt ausschließlich Build-Artefakte aus (build, .gradle, local.properties, /.tauri, /tauri.settings.gradle, app/src/main/**/generated, app/src/main/jniLibs/**/*.so, app/src/main/assets/tauri.conf.json). AndroidManifest.xml und MainActivity.kt sind getrackte Quelldateien ohne THIS IS AN AUTOGENERATED FILE-Header.

Maintainer FabianLars, Discussion #8323 (01.12.2023):

„Generally yes. it has .gitignore folders inside that ignores the build artifacts etc. The gen folder also contains the normal android studio / xcode projects which you can modify if you want which would require you to commit it."

tauri android build/dev überschreibt die Datei nicht; tauri android init rendert die Templates neu und ist damit als destruktiv zu behandeln. Siehe Experiment E3.

4.5 Secure Context

Tauri liefert auf Android und Windows unter http://tauri.localhost aus. Verifiziert in crates/tauri-utils/src/config.rs:

/// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default
/// `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
pub use_https_scheme: bool,

und im Default-Impl: use_https_scheme: false.

Chromium behandelt *.localhost auch über plain http als vertrauenswürdig. net/base/url_util.cc:

bool IsNormalizedLocalhostTLD(std::string_view host) {
  return base::EndsWith(host, ".localhost", base::CompareCase::INSENSITIVE_ASCII);
}

Deckt sich mit W3C Secure Contexts, Schritt 5: „origin's host ends with .localhost or .localhost.". Das System WebView ist Chromium und linkt denselben //services/network-Code.

Gegenstimme, die ich für falsch halte: wry#1709 / #1710 (01.04.2026) behaupten, Android WebView halte nur exakt http://localhost, https:// und file:// für sicher. Beide Issues stammen von einem Nicht-Maintainer, #1709 hat null Antworten, und die Behauptung wird ohne Chromium-Quelle aufgestellt — im direkten Widerspruch zum Code oben. Die dort angeführten Belege (SharedWorker, Service Worker) haben andere, bekannte Ursachen. Trotzdem: Experiment E4 kostet zehn Sekunden.

Risikoloser Hedge: useHttpsScheme: true unter app.windows[]. Nachteile laut Tauri-Doc-Kommentar: kein Mixed Content mehr (http://-Fetches brechen), und die IndexedDB-/Cookie-/localStorage-Partition wandert. Also vor dem ersten Release entscheiden, nicht danach.


5. Linux (WebKitGTK 4.1) — Nein

Tauri v2 verlangt auf Linux webkit2gtk-4.1 (libwebkit2gtk-4.1-dev / webkit2gtk-4.1 / webkit2gtk4.1-devel, v2.tauri.app/start/prerequisites). Dort scheitert WebRTC an drei unabhängigen Gates, die alle passiert werden müssen.

Gate 1 — Compile-Time: ENABLE_WEB_RTC ist standardmäßig AUS

Source/cmake/OptionsGTK.cmake:

WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MEDIA_STREAM PRIVATE ON)
WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEB_RTC     PRIVATE ${ENABLE_EXPERIMENTAL_FEATURES})

Source/cmake/WebKitFeatures.cmake:

option(ENABLE_EXPERIMENTAL_FEATURES "Enable experimental features" OFF)

⇒ RTCPeerConnection ist im JS-Global schlicht nicht vorhanden, sofern der Packager nicht explizit -DENABLE_WEB_RTC=ON setzt. ENABLE_MEDIA_STREAM ist zwar an (also getUserMedia grundsätzlich da), aber ohne ENABLE_WEB_RTC nützt das nichts.

Verifizierter Distro-Stand:

  • Debian/Ubuntu webkit2gtk — debian/rules enthält weder ENABLE_WEB_RTC noch ENABLE_EXPERIMENTAL_FEATURES. ⇒ AUS. (sources.debian.org)
  • Alpine community/webkit2gtk-4.1 APKBUILD — dito. ⇒ AUS.
  • Arch / Fedora — nicht verifiziert; beide Dist-Git-Hosts sind hinter einer Anubis-Bot-Schranke (Experiment E5). Schwaches Indiz gegen WebRTC bei Arch: libnice taucht in den Dependencies nicht auf.

Korroborierend: tauri#13143 „The current browser does not support WebRTC" auf Ubuntu 22.04 mit WebKit2GTK 2.48.0 — geschlossen als not planned, Label status: upstream (05.04.2025).

Historie: Der GstWebRTC-Backend kam mit WebKitGTK 2.37.1 („Add initial implementation of WebRTC using GstWebRTC if GStreamer 1.20 is available"). Es gibt keinen NEWS-Eintrag, der ein Default-On dokumentiert.

Gate 2 — Runtime-Setting: enable-webrtc ist standardmäßig FALSE

WebKitSettings:enable-webrtc:

„Enable WebRTC support for loaded pages. Enabling this setting implies that WebKitSettings:enable-media-stream will be enabled as well." Default value: FALSE. Available since: 2.38.

wry setzt es nie — weder in 0.55.1 noch in dev. In set_webview_settings (src/webkitgtk/mod.rs) stehen set_enable_webgl, set_enable_webaudio, set_enable_page_cache u. a., aber kein set_enable_webrtc und kein set_enable_media_stream. Tauri exponiert die WebKitSettings auch nicht (tauri#8989, [feat] expose control over webkit settings, offen).

Gate 3 — Permission: Default ist DENY

WebKitWebViews Default-Class-Handler für permission-request (Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp) verweigert alles außer Pointer-Lock. Die Doku ist explizit (WebKitUserMediaPermissionRequest):

„When a WebKitUserMediaPermissionRequest is not handled by the user, it is denied by default."

wry 0.55.1 verbindet connect_permission_request überhaupt nicht — im ganzen src/webkitgtk/mod.rs kommt der String nicht vor. Das erklärt tauri#12547 („access denied, no dialog", geschlossen not planned, 28.01.2025).

wry 0.56+ verbindet ihn — aber nur, wenn die App einen Handler übergibt:

// Permission handler
if let Some(permission_handler) = attributes.permission_handler.take() {
  webview.connect_permission_request(move |_webview, request| {
    if let Some(media_request) = request.downcast_ref::<UserMediaPermissionRequest>() { ... }

Und docs.rs zu PermissionResponse::Default ist unmissverständlich: „Windows / macOS / Android: continue the platform or browser permission flow. Linux: The default behavior is Deny."

Der Weg, der theoretisch bliebe

WebviewWindow::with_webview gibt auf Linux Zugriff auf die rohe webkit2gtk::WebView (docs.rs/tauri). Damit ließen sich Gate 2 (settings.set_enable_webrtc(true), Binding-Feature v2_38) und Gate 3 (connect_permission_request) aus dem App-Code heraus schließen — ohne wry oder Tauri zu patchen.

Gate 1 bleibt trotzdem. Ist WebRTC nicht einkompiliert, hilft kein Setting. Und Gate 1 hängt am Distro-Paket des Nutzers, nicht an unserem Code.

Konsequenz für die Map

Die harte Deployment-Regel („eine Binary + Postgres") betrifft den Server. Für den Client auf Linux gibt es keine analoge Zusage — aber die Zielgruppe „Selbsthoster" ist stark linux-lastig, und Linux ist in der Map als Zielplattform gelistet. Realistische Optionen, alle unattraktiv:

  1. Voice auf Linux als nicht unterstützt deklarieren. Chat läuft voll, Voice nicht. Ehrlich, billig, schmerzhaft.
  2. Nativer Pfad nur auf Linux (§6). Bricht die Architektur-Einheitlichkeit — Voice läge auf drei Plattformen im WebView und auf einer in Rust.
  3. Eigenes WebKitGTK bündeln. Widerspricht der Simplizitätsdoktrin des Projekts frontal.
  4. Warten/upstream drücken: wry ≥ 0.56 + Tauri ≥ 2.12 (PR #14865, gemergt 03.08.2026, Milestone 2.12, noch nicht released) lösen Gate 3 sauber. Gate 2 bräuchte zusätzlich with_webview-Code oder tauri#8989. Gate 1 löst niemand außer den Distros.

Diese Entscheidung gehört als Ergebnis in Ticket 04 (Voice-Topologie), nicht hierher.


6. Windows (WebView2) — Ja, ohne Konfiguration

  • Engine: Chromium. Microsoft dokumentiert navigator.mediaDevices.getUserMedia() explizit als in WebView2 verfügbar, mit Secure-Context als einziger Einschränkung (Working with local content).
  • Permission-Default ist PROMPT, nicht Deny. COREWEBVIEW2_PERMISSION_STATE_DEFAULT — „Specifies that the default browser behavior is used, which normally prompt users for decision." get_State — „The default value is COREWEBVIEW2_PERMISSION_STATE_DEFAULT." Permission-Kind: COREWEBVIEW2_PERMISSION_KIND_MICROPHONE — „Indicates permission to capture audio." API vorhanden seit WebView2 0.9.430; jede GA-Runtime genügt.
  • wry 0.55.1 registriert zwar add_PermissionRequested, setzt aber nur für COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ einen State (verifiziert in src/webview2/mod.rs). Alles andere bleibt auf STATE_DEFAULT ⇒ WebView2 zeigt seinen eigenen Prompt.
  • Secure Context: http://tauri.localhost — siehe §4.5. Kein --unsafely-treat-insecure-origin-as-secure nötig.

Eine Falle: Die Entscheidung wird im WebView2-Profil persistiert. Wer einmal „Blockieren" klickt, bekommt nie wieder einen Prompt, bis %LOCALAPPDATA%\<bundleId>\EBWebView\Default\Preferences gelöscht wird (tauri#5042, #8979). Das muss die UI abfangen: getUserMedia, das ohne sichtbaren Dialog sofort mit NotAllowedError zurückkommt, ist ein anderer Zustand als „Nutzer hat gerade abgelehnt", und der Nutzer braucht eine Anleitung statt eines Timeouts.

Für MSIX/UWP-Pakete zusätzlich: microphone-Device-Capability im AppxManifest.


7. Nativer Fallback und Sicherheits-Eigenheiten

7.1 webrtc-rs — reiner Protokoll-Stack, keine Audio-Erfassung

webrtc 0.20.2 (11.08.2026), 5,6 Mio. Downloads, github.com/webrtc-rs/webrtc. README-Selbstbeschreibung: „Sans-I/O protocol core with complete WebRTC stack (95%+ W3C API compliance)" — ICE, DTLS, SRTP, SCTP, RTP, DataChannels, Simulcast.

Nicht enthalten: Geräte-Capture, Codecs, Jitter-Buffer/NetEQ, Echo-Cancellation. Der Maintainer selbst (webrtc-rs#94) beschreibt das Ersetzen von Google-libwebrtc als „requires porting NetEQ, audio 3A (AEC, AGC, NS), audio/video codec" — also ausdrücklich nicht getan.

Der vollständige Eigenbau bräuchte zusätzlich: cpal 0.18.1 (Capture), opus/audiopus (Encoder), RTP-Packetisierung, einen selbstgeschriebenen Jitter-Buffer mit PLC — und für AEC/AGC/NS ausgerechnet webrtc-audio-processing, das seinerseits ein C++-Binding an Googles APM ist. Man entkommt libwebrtc also nicht, man baut nur den unangenehmen Teil selbst.

Mobile: keine dokumentierte Plattform-Matrix. Android wird real benutzt, mit bekannten Kanten bei Netzwerk-Interfaces und Handover (webrtc-rs #874, #272, #78). iOS: #645 „build a static lib for ios/macos" wurde als not planned geschlossen.

7.2 livekit/rust-sdks (libwebrtc-Crate) — die realistische native Option

libwebrtc 0.3.45 (10.08.2026), livekit 0.8.3 — github.com/livekit/rust-sdks, „LiveKit safe bindings to libwebrtc". README listet Windows, macOS, Linux, iOS, Android, mit Mobile-Beispiel. Liefert echtes libwebrtc: NetEQ, AEC/AGC/NS, Opus, Geräte-Capture, HW-Encoding.

Preis: ein großer vorkompilierter nativer Blob pro Plattform/Architektur plus LiveKits Build-Toolchain. Das steht in offener Spannung zur Simplizitäts-Doktrin des Projekts.

7.3 Der eigentliche Aufwand liegt in der Rust↔WebView-Naht

Geht Audio nativ, ist der WebView nicht mehr der Medien-Endpunkt: Mikrofon-Capture in Rust, Remote-Wiedergabe in Rust (kein <audio>), Signaling/State über Tauri-IPC, und für die UI (Pegelanzeige, Speaking-Indikator) nur heruntergerechnete Metering-Werte über IPC — niemals rohes PCM. Zurück-Injizieren von PCM in den WebView via WebAudio/MediaStreamTrackGenerator geht in Chromium, aber nicht in WebKitGTK — auf Linux müsste der native Pfad die Wiedergabe also komplett besitzen.

Grobe Größenordnung: livekit/libwebrtc ≈ Tage bis zum funktionierenden Call mit brauchbarer AEC; webrtc-rs + cpal + opus + APM ≈ Wochen, und man besitzt die Echo-Cancellation-Qualität auf Dauer selbst.

Empfehlung: Der native Pfad ist als Fallback für Linux diskutabel, nicht als Ersatz für den WebView-Pfad. Für Mobile ist er unnötig — dort funktioniert der WebView.

7.4 Bedrohungsmodell: der Signaling-Server kann DTLS-Fingerprints tauschen

Das ist der wichtigste Sicherheitsbefund für ein Projekt, dessen Betreiber als Angreifer gilt.

RFC 8827 (WebRTC Security Architecture, Januar 2021) benennt den Angriff wörtlich:

„any on-path attacker can replace the DTLS-SRTP fingerprints in the handshake and thus substitute its own identity for either endpoint"

Der Server relayed die SDP. Er kann a=fingerprint: austauschen und zwei separate DTLS-Sitzungen terminieren — vollständiges MITM auf dem Audio, ohne dass irgendein WebRTC-Mechanismus anschlägt.

Der spezifizierte Gegenmechanismus ist tot. W3C Identity for WebRTC 1.0 steht seit 27.09.2018 unverändert als Candidate Recommendation. MDN-Browser-Compat für setIdentityProvider / peerIdentity / getIdentityAssertion: Chrome false, Safari false, WebView Android false, nur Firefox 40. ⇒ In keiner der vier Ziel-Engines verfügbar.

Was stattdessen zu tun ist — und was ins Signaling-Protokoll (Ticket 09) gehört:

  1. Fingerprint deterministisch vorab bestimmen: RTCPeerConnection.generateCertificate() + RTCCertificate.getFingerprints(), statt ihn erst aus localDescription.sdp zu lesen.
  2. Den Fingerprint durch den E2EE-Kanal der App übertragen, signiert mit dem Langzeit-Identity-Key — nicht im Klartext-SDP, das der Server relayed.
  3. Beim Empfang gegen den Fingerprint im relayed SDP vergleichen. Mismatch ⇒ Call abbrechen und als Sicherheitsereignis anzeigen, nicht still weiterlaufen lassen.
  4. Key Continuity (RFC 8827): Fingerprints pro Gerät über Calls hinweg cachen und bei Änderung warnen.
  5. Optional eine SAS (Emoji/Wortliste aus dem Hash beider Fingerprints, à la ZRTP/Signal) für den paranoiden Pfad — die verteidigt zusätzlich gegen einen kompromittierten Identity-Key.

Genau das macht Matrix in MSC3401/Element Call: Call-SDP läuft über m.room.encrypted To-Device-Messages, der Homeserver sieht den Fingerprint nie im Klartext. Hier ist die eine Stelle, an der das Voice-Signaling-Protokoll heute schon eine E2EE-Naht braucht — auch wenn das volle E2EE-Design ein Folge-Effort ist. Das gehört als Randbedingung in Ticket 09.

7.5 IP-Leaks, TURN-Metadaten, CSP

  • iceTransportPolicy: "relay" (W3C WebRTC 1.0): „The ICE Agent uses only media relay candidates"; Spec-Note: „can be used to prevent the remote endpoint from learning the user's IP addresses". Versteckt die IP vor dem Peer, nicht vor dem TURN-Server. Da der Server die IP ohnehin aus der WebSocket-Verbindung kennt, ist relay nahezu gratis — es reduziert strikt die Exposition gegenüber dem Peer. Relevant für Ticket 04.
  • mDNS-ICE-Kandidaten (.local): IETF-Draft draft-ietf-mmusic-mdns-ice-candidates ist abgelaufen (-03, Juni 2022, nie RFC) — trotzdem überall ausgeliefert: Chromium ab M74 (2019), WebKit ab bug 209050 (RESOLVED FIXED, März 2020). WebKit ist strenger als Chromium: Host-Kandidaten werden bis zur Capture-Erlaubnis komplett unterdrückt (webkit.org/blog/7763, 03.07.2017). Ob die .local-Namen im Android-WebView auch auflösbar sind (Multicast braucht dort einen MulticastLock), ist ungeprüft — Experiment E6.
  • Was ein feindlicher TURN-Betreiber unter relay lernt: nicht den Klartext (DTLS-SRTP bleibt Ende-zu-Ende), aber beide öffentlichen IPs, exakt wer-mit-wem, Start/Ende/Dauer, und über Paketgrößen eine Voice-Activity-Detection. Opus läuft per Default variabel-bitratig; das leckt Sprech-/Pausen-Muster. Gegenmaßnahme: cbr=1 (und ggf. usedtx=0) in der Opus-fmtp-Zeile, gegen Bandbreite eingetauscht. Gehört als Notiz zu Ticket 04/09, auch wenn Codecs formal out of scope sind — es ist eine SDP-Frage.
  • CSP kann WebRTC nicht einschränken. Der webrtc-Direktive existiert in CSP Level 3 (Working Draft, 13.08.2026, §6.2.1: „The webrtc directive controls whether a global object may initiate WebRTC connections"), ist aber nirgends implementiert: Firefox Bugzilla 1783489 NEW/unassigned, Chromium 40188662 noch im „evaluate"-Stadium, MDN-Doku-Issue als not planned geschlossen. connect-src greift bei RTCPeerConnection nicht — genau deshalb brauchte es überhaupt eine eigene Direktive. Ein harter Kill-Switch geht nur JS-seitig (Konstruktor vor dem App-Code entfernen).

7.6 WebView-spezifische Eigenheit: wry vergibt Mikrofonrechte ohne App-Beteiligung

Aus §3.3 und §4.2 folgt eine Eigenschaft, die man kennen sollte:

  • iOS/macOS (wry 0.55.1): WKPermissionDecision::Grant, bedingungslos, für jeden Origin und jeden Frame. Es gibt in dieser Version keinen Hook, um das zu verhindern.
  • Android (wry 0.55.1/0.56): RESOURCE_AUDIO_CAPTURE steht in der Menge der bekannten, automatisch weitergereichten Ressourcen.

Solange das Frontend ausschließlich gebündelter, eigener Code auf tauri://localhost bzw. http://tauri.localhost ist, ist das Risiko gering — der feindliche Server liefert kein Skript aus. Es wird sofort relevant, sobald irgendwann remote Inhalte, ein <iframe>, eine Vorschau-Einbettung oder ein <webview> für fremde Inhalte dazukommt: Dann bekäme dieser Origin Mikrofonzugriff, ohne dass die App gefragt würde. Für eine datenschutzorientierte App ist das eine Regel, die aufgeschrieben gehört: keine fremden Origins im selben WebView.

Ab Tauri 2.12 (wry ≥ 0.56, PR #14865) lässt sich das über on_permission_request sauber kontrollieren. Bis dahin ist die Architektur-Regel der einzige Schutz.

7.7 Insertable Streams / SFrame — falls je ein SFU kommt

W3C WebRTC Encoded Transform (Working Draft, 25.06.2026), inkl. SFrame nach RFC 9605. Support (MDN BCD):

Engine RTCRtpScriptTransform
Safari / WKWebView (iOS) 15.4
Chrome / Android WebView / WebView2 141 (~Okt. 2025)
Firefox 117
Chromium-Legacy createEncodedStreams() 86 (nicht standardisiert, WebKit hat es nie)

Wichtige Einschränkung für das Bedrohungsmodell: Der SFU behält auch mit E2EE Zugriff auf RTP-Metadaten — „synchronization sources, payload types, timestamps, and audio levels". Die audio-level-Header-Extension ist ein direktes Voice-Activity-Orakel; sie müsste beim SFU-Einsatz wegverhandelt werden.

Für die im Ticket 04 zur Debatte stehende Mesh-Topologie ist das alles irrelevant — dort ist DTLS-SRTP schon Ende-zu-Ende. Es wird erst mit einem SFU nötig.

7.8 enumerateDevices()

Spec-konform sind label-Felder vor der Erlaubnis leer; deviceId/groupId sind aber pro Origin stabile Hashes, und Anzahl und Art der Geräte lecken vor der Erlaubnis. In einer Tauri-App ist das konstruktionsbedingt kein Angriffsvektor (das Frontend ist eigener Code). Die daraus folgende Regel ist eine andere: Geräte-Inventar niemals an den Server senden — nicht in Telemetrie, nicht in Logs, nicht in Crash-Reports. Und SDP ist selbst ein starker Fingerabdruck (Codec-Liste, Header-Extensions, a=msid, in Chromium a=ssrc cname); wo möglich reduzieren.


8. Offen — und welches Experiment es entscheidet

Nichts davon blockiert die Spec-Arbeit; alles davon sollte vor der Implementierung geklärt sein.

E1 — iOS: funktioniert getUserMedia real auf einem Gerät? (höchste Priorität) Ist die einzige Frage, die noch ein offenes Upstream-Issue hat (tauri#12068). Minimale Tauri-v2-App zweimal bauen — einmal mit, einmal ohne NSMicrophoneUsageDescription — auf einem physischen iPhone (iOS ≥ 15). In beiden Fällen loggen: typeof navigator.mediaDevices, window.isSecureContext, location.origin, await navigator.mediaDevices.enumerateDevices(), dann getUserMedia({audio:true}) aus einem Click-Handler. Erwartung: isSecureContext === true, origin === "tauri://localhost", Erfolg nur in der Variante mit Key. Zusätzlich prüfen, ob der Key nach dem Merge tatsächlich in gen/apple/*/Info.plist steht (tauri#13068 warnt vor stillem Überschreiben).

E2 — Android: ist MODIFY_AUDIO_SETTINGS wirklich zwingend? Dieselbe App dreimal: nur RECORD_AUDIO / nur MODIFY_AUDIO_SETTINGS / beide. Erwartung nach Code-Lesung: nur die dritte Variante liefert einen Stream; die erste bringt NotAllowedError nachdem der Nutzer im Dialog zugestimmt hat. Entscheidet zugleich tauri#10846.

E3 — Überlebt das hand-editierte AndroidManifest.xml? Auf einem Scratch-Checkout tauri android init, Manifest editieren, tauri android build, diffen; dann tauri android init erneut und wieder diffen. Erwartung: Build lässt es in Ruhe, init rendert neu.

E4 — Ist http://tauri.localhost im Android-WebView ein Secure Context? Über chrome://inspect in der laufenden App auswerten: window.isSecureContext und !!window.crypto.subtle. Beide true ⇒ wry#1709 ist widerlegt und useHttpsScheme bleibt aus.

E5 — Haben Arch und Fedora WebKitGTK mit -DENABLE_WEB_RTC=ON gebaut? Für Debian/Ubuntu und Alpine ist die Antwort belegt (nein). Für Arch/Fedora blockte die Bot-Schranke. Distro-unabhängiger und definitiver Test auf einer beliebigen Maschine: nm -D --defined-only /usr/lib/*/libwebkit2gtk-4.1.so.0 | grep -i rtcpeerconnection — oder in einem beliebigen WebKitGTK-View typeof RTCPeerConnection === 'function'.

E6 — Lösen sich mDNS-.local-ICE-Kandidaten im Android-WebView und in WKWebView auf? Zwei echte Geräte im selben LAN, pc.onicecandidate loggen, prüfen ob typ host-Kandidaten auf .local enden und ob die Verbindung trotzdem zustande kommt.

E7 — Wann kommt Tauri 2.12? Trägt PR #14865 (on_permission_request), gemergt 03.08.2026. crates.io tauri steht auf 2.11.5. Relevant für Linux Gate 3 und für §7.6.


9. Konfigurationsschritte, kompakt

src-tauri/Info.ios.plist                              [NEU]
  NSMicrophoneUsageDescription = "…"

src-tauri/gen/android/app/src/main/AndroidManifest.xml [EDITIEREN, committen]
  + android.permission.RECORD_AUDIO
  + android.permission.MODIFY_AUDIO_SETTINGS
  + uses-feature android.hardware.microphone (required=false)

src-tauri/tauri.conf.json                              [UNVERÄNDERT]
  csp: null            — passt, CSP betrifft WebRTC ohnehin nicht
  useHttpsScheme       — nicht setzen, außer E4 fällt negativ aus (dann VOR dem ersten Release)
  bundle.iOS.minimumSystemVersion: mindestens "14.3", praktisch "15.0"

Windows                                                [NICHTS ZU TUN]
Linux                                                  [NICHT LÖSBAR — siehe §5, Entscheidung in Ticket 04]

10. Quellen

Apple / WebKit

Android / Chromium

Microsoft / WebView2

WebKitGTK

Tauri / wry (jeweils Tag wry-v0.55.1, sofern nicht anders vermerkt)

Tauri-Issues und -Discussions (Hinweisgeber, mit Datum und Status)

Standards

Rust-Ökosystem