# Eine Tour durch das URnetwork SDK

Das ist der Tiefgang hinter [Erste Schritte](/docs/getting-started-sdk): wie
jede Binding-Schicht zusammengesetzt ist, welche Standardwerte sie
ausliefert und welche scharfen Kanten wir kennen. Alles läuft gegen
URnetworks gehostete Plattform. Du bringst Zugangsdaten mit, keine
Infrastruktur. Das Objektmodell (Device, API, View-Controller) steht in
[/docs/overview](/docs/overview) und [/docs/api](/docs/api).

Ein Go-Kern implementiert den gesamten Client: Transport, Verträge,
Paketpfad. Jedes Artefakt (AAR, xcframework, wasm, c-shared-Bibliothek) ist
ein Binding genau dieses Kerns — deshalb ist das Verhalten über alle
Plattformen hinweg identisch, und deshalb sind die Artefakte groß (jedes
trägt die Go-Runtime). Der Kern trägt auch Produktregeln, nicht nur Pakete.
Solana Pay lebt hier (`CreatePaymentReference`, `BuildSolanaPaymentUrl`),
weil es das einst nicht tat: Die Web-App prägte die Zahlungsreferenz als
Hex-UUID, wo Solana Pay einen base58-kodierten 32-Byte-Pubkey verlangt — ein
Kunde konnte also zahlen und wurde nie wieder seinem Konto zugeordnet —, und
Android hatte Betrag und Händleradresse hart verdrahtet. Eine Regel neu zu
implementieren, der die Apps längst folgen, ist die Klasse von Bug, gegen
die diese Schicht existiert.

## Mobile Bindings

### DeviceLocal und die App-Prozess-Aufteilung auf Apple

`DeviceLocal` ist das echte Ding, die laufende Client-Engine: Ihm gehören
der Transport, der Vertragszustand (URnetworks Buchführung über die Bytes,
die ein Konto bewegen darf) und der Paketpfad. `DeviceRemote` ist ein Client
für ein `DeviceLocal`, das in einem anderen Prozess oder auf der Plattform
lebt, mit derselben API-Fläche: eine Fernbedienung für das echte Device. Auf
Android beherbergt ein Prozess sowohl die UI als auch den `VpnService`, die
App hält das `DeviceLocal` also direkt.

Apple führt die VPN-Paketverarbeitung in einem eigenen, gesandboxten
NetworkExtension-Prozess aus (jedes iOS-/macOS-VPN ist so aufgeteilt), also
teilt sich das SDK mit ihm auf:

- Die `NEPacketTunnelProvider`-Extension besitzt das `SdkDeviceLocal` und
  ruft darauf `setRpcServer(serverPem, clientCertPem, hostPort)` auf, um den
  Listener zu starten.
- Der App-Prozess erstellt ein `SdkDeviceRemote` und hängt sich über eine
  **Loopback-mTLS-Device-RPC**-Verbindung an: das spiegelbildliche
  `setRpcServer(clientPem, serverCertPem, hostPort)`, wobei jede Seite
  zuerst ihre eigene Identität und danach das Zertifikat der Gegenseite
  benennt. Device-RPC ist das eigene Protokoll des SDK: Methodenaufrufe plus
  Subscription-Streams über Gos net/rpc, nicht gRPC. Gegenseitiges TLS auf
  Loopback ist keine Zeremonie. Localhost ist für jeden lokalen Prozess und
  jeden lokalen Nutzer erreichbar, und nur wer die passenden PEMs hält, kann
  sich anhängen.
- **Die App prägt das Key-Material, nicht die Extension.**
  `SdkGenerateDeviceRpcKeyMaterial()` gibt pro VPN-Sitzung ein frisches,
  selbstsigniertes Server- und Client-Schlüsselpaar zurück; die App legt
  beide PEMs und den host:port in die **Provider-Konfiguration** des
  `NETunnelProviderProtocol`, und so bekommt die Extension sie beim Start des
  Tunnels. Ein App-Group-Container ist nicht beteiligt.
- Beobachte den `RemoteChangeListener`: Wenn `remoteConnected` zum ersten
  Mal auf true geht, ist dieses Material erwiesen — persistiere es also als
  zuletzt bekannt gut und wende es beim nächsten Start wieder an. Genau das
  lässt die App sich an eine bereits laufende Extension anhängen.

Rebuilds sind Routine: Die Extension baut ihr Device jedes Mal neu, wenn der
Tunnel neu startet, weil der Nutzer das VPN umschaltet, das OS die Extension
neu startet oder iOS sie killt, weil sie das Speicherlimit für Extensions
überschritten hat. Verdrahte dieses Wieder-Anhängen (prägen, Tunnel starten,
`setRpcServer`, den eigenen UI-Zustand wieder anwenden) am ersten Tag.
Alles, was du auf einem `DeviceLocal` tun kannst, kannst du auf einem
`DeviceRemote` tun; das Remote leitet Aufrufe weiter und spielt
Subscriptions über Wiederverbindungen hinweg erneut ein. Zwei Stufen: Eine
schlichte **Wiederverbindung** (dasselbe Device, die RPC-Verbindung ist
abgerissen und zurückgekommen) stellt die Subscriptions für dich wieder her;
eine **Neuerstellung** (eine neue Device-Instanz darunter) heißt, diese
Sequenz erneut zu fahren. Eine Falle: `DeviceRecreatedListener` feuert bei
einem Wechsel der *Device-Generation*, und nur der plattformgehostete
RPC-Pfad stempelt eine — auf Loopback treibst du das Wieder-Anhängen also
aus deinem eigenen Tunnel-Lebenszyklus und dem `RemoteChangeListener`.

### IoLoop (nur Android)

`Sdk.newIoLoop(device, detachedFd)` ist die Paketpumpe:

- Übergib einen **detachten, nicht blockierenden** fd
  (`ParcelFileDescriptor.detachFd()`). Nach diesem Aufruf **gehört der fd
  Go, und Go wird ihn schließen**; wickle ihn nie wieder aus Java ein und
  schließ ihn nicht. Zwei Besitzer heißt Double-Close: fd-Nummern werden
  sofort wiederverwendet, ein verirrtes Schließen aus Java kann also
  irgendeinen unbeteiligten Deskriptor zertrampeln, der die Nummer als
  Nächstes bekommen hat — Korruption weit weg von ihrer Ursache.
- Die Schleife pumpt beide Richtungen (tun→Device und Device→tun) innerhalb
  von Go, was JNI-Übergänge und Pufferkopien pro Paket vermeidet. Deshalb
  nimmt die API einen rohen fd, statt Read-/Write-Methoden anzubieten.
- Zum Stoppen schließt du den IoLoop (nicht den fd); der Done-Callback
  feuert, wenn die Schleife endet. Feuert er, ohne dass du gefragt hast —
  weil der tun-fd EOF oder einen Fehler traf oder das Device
  heruntergefahren wurde —, behandle es als „der Tunnel ist weg“: Beende die
  `VpnService`-Sitzung, dann bau ihn neu auf oder zeig „getrennt“. Der
  Callback trifft auf einem SDK-Thread ein, und Go schließt den fd auf dem
  Weg hinaus; fass ihn im Handler nie an.

Apple nutzt IoLoop nicht: Der Tunnel-Provider bewegt Pakete über
`packetFlow`, die einzige Schnittstelle, die NetworkExtension anbietet.
Jedes iOS-VPN zahlt diesen Übergang, und seine gebündelten Reads
amortisieren die Kosten.

### Provide-Modi, und warum Key-Material zählt

`SetProvideMode` steuert, ob das Device dem Netzwerk Kapazität anbietet. Der
Standard ist aus: Ein frisch konstruiertes Device hat den Provide-Modus
„none“ und bietet nichts an, bis du einen Modus setzt — das SDK
einzubetten teilt die Bandbreite deiner Nutzer also nie still. Zwei Modi tun
heute etwas: **public** und **network**, das die Bereitstellung auf andere
Geräte im selben URnetwork-Konto beschränkt. Das Protokoll-Enum trägt mehr
Werte, aber die Plattform löst jeden Peer außerhalb deines Netzwerks zu
public auf — die echte Wahl ist also: aus, meine eigenen Geräte, oder alle.

Beim Bereitstellen zählt die **Persistenz des Key-Materials**: Das
`DeviceLocalKeyMaterial`, das du bei der Konstruktion übergibst, *ist* die
Provider-Identität des Device, ein Client-Key-Seed plus das
Provide-TLS-Zertifikat und sein Schlüssel. Persistiere es im sicheren
Speicher der Plattform (Keystore, Keychain) und übergib bei jedem Start
dasselbe Material, sonst sieht das Netzwerk jedes Mal einen brandneuen
Provider und verliert die Zuverlässigkeits-Historie, die die Auswahl
bevorzugt. Flüchtiges (`null`) Material ist für reine Clients fein.

Wenn du das Bereitstellen anbietest, benenne den Tausch: Der Datenverkehr
Fremder tritt über die IP des Nutzers aus, und die Provider-Position sieht
Ziel-IPs und TLS-SNI (wie ein Internetanbieter), standardmäßig aber nicht
die echte IP des ursprünglichen Nutzers. Die Sicherheit des Providers ist in
der Engine konstruiert. Die quelloffene ip_security-Schicht inspiziert den
eigenen Ausgang des Providers und verwirft Datenverkehr der DMCA- und der
CFAA-Klasse, bevor er ihn verlässt. Das Urteil ist ein verworfenes Paket,
ohne dass irgendwo ein Ziel, eine Domain oder Inhalte aufgezeichnet werden.
Ein Treffer auf eine BitTorrent-Signatur sendet zusätzlich eine
Missbrauchsmarkierung an den Operator, die nur die Geräte-ID des Peers und
einen Boolean trägt, und der Operator liefert heute keinen Handler dafür
aus; Verwürfe opak verschlüsselten Datenverkehrs bleiben still. Provider
nehmen am UR Protocol teil; [ur.xyz](https://ur.xyz) dokumentiert die
Vergütung.

### Laufzeit-Notizen

- GC-Pacing wird automatisch pro OS abgestimmt: ein Pacing-Faktor von 10 auf
  iOS (die Speicherlimits für Extensions sind brutal) gegenüber 50 auf
  Android und 100 anderswo. Du setzt ihn nicht.
- `memoryTargetByteCount` ist eine andere Stellschraube: ein Byte-Budget,
  das das Device *aufteilt*, um seine eigenen Puffer zu bemessen, dns 2 :
  client 14 : provider 4, wobei der Provider-Anteil das Client-Paar stützt,
  solange das Bereitstellen aus ist (Standard 20 MB). Weder eine Obergrenze
  noch eine GC-Einstellung: Das prozessweite weiche Footprint-Limit ist das
  separate `SetMemoryLimit`, und der harte Kill ist das Extension-Limit des
  OS. Setz das Target darunter; halt die Extension nahezu logikfrei.
- Der gomobile-Bind ist im Build gegatet, und der Mechanismus zählt: gobind
  lässt still alles weg, was es nicht binden kann, und hinterlässt in den
  generierten Quellen nur einen `// skipped`-Kommentar — der Build grept
  also diese Quellen und **schlägt bei jeder Auslassung fehl, die nicht auf
  einer expliziten Allowlist steht**. Die erlaubten Skips sind intern
  (RPC-gob-Payloads, die Proxy-/Plattform-Fläche, `uint64`- und
  `[][]byte`-Formen, die gomobile nicht ausdrücken kann): Absicht, keine
  Drift.

## JavaScript

### Warum es im wasm kein DeviceLocal gibt

Eine Browser-Seite kann kein tun-Interface besitzen, ein wasm-`DeviceLocal`
hätte also nichts zu pumpen. Die JS-Schicht liefert nur die clientseitigen
Hälften: die API-Fläche, die View-Controller und `DeviceRemote`, einen
vollen Device-*Client*, dessen Device anderswo lebt. Die Seite hält die
Steuerungsebene (Verbindungszustand, Standorte, Statistiken, Konto), während
der Datenverkehrspfad von allem konsumiert wird, was den gehosteten Proxy
nutzen kann. Daher die zwei Factories: `createProxyDevice` ist das dünne
Modell, das gehostete Proxy-URLs auflöst und den Datenverkehr allem
überlässt, was sie konsumiert (eine Proxy-Konfiguration einer Erweiterung,
ein Fetch-Agent); `createPlatformDeviceRemote` ist das dicke Modell, ein
echtes `DeviceRemote` mit der vollen Listener- und View-Controller-Fläche
über ein Device, das die Plattform für dich hostet.

### signedProxyId-Auth

`createPlatformDeviceRemote` öffnet einen Device-RPC-WebSocket zu
`wss://<proxy>/device-rpc`. Dieser WebSocket authentifiziert sich **nicht**
mit dem Konto-JWT (`byJwt`, dem Bearer-Token aus dem URnetwork-Login; das
„by“ ist ein Überbleibsel von BringYour). Er authentifiziert sich mit
`signedProxyId`, dem `auth_token`, das der Plattform-Endpunkt
`/network/auth-client` neben der Proxy-URL zurückgibt. Das ist bewusste
Trennung der Geltungsbereiche: Die signierte Proxy-ID autorisiert genau
einen WebSocket und trägt keine Konto-Autorität — das breite Token fährt
also nie auf dem Socket der Datenebene mit. Behandle (`proxyUrl`,
`signedProxyId`) als einen einzigen Zugangsnachweis. Fordere sie zusammen
an, gib sie zusammen weiter, frisch sie zusammen auf, und fordere das Paar
neu an, wenn der Proxy den Socket ablehnt, statt ein Teilstück zu cachen.

### Listener-Muster und View-Controller

Jede Subscription hat dieselbe Form: `add*ChangeListener(fn)` gibt eine
**Unsubscribe-Funktion** zurück. Halt sie fest und ruf sie beim Abbau auf.
In React gibst du sie zurück:

```js
useEffect(() => {
  const unsub = device.addConnectChangeListener(setConnectEnabled);
  return unsub;
}, [device]);
```

Das übersteht die doppelt aufgerufenen Effects im Strict Mode von React
(add, unsub, add) und lässt genau eine lebende Subscription übrig. Die
View-Controller-Fläche (Connect, Standorte, Geräte, Verträge,
Block-Aktionen) ist ebenfalls ins wasm gebunden und hängt am Device, wie in
`device.openConnectViewController()` — eine Web-App nutzt also dieselbe
Präsentationslogik wieder wie die mobilen Apps, statt den Zustand erneut aus
rohen Listenern abzuleiten.

### Wie ur.io es nutzt

Die `/app`-Fläche auf ur.io ist der Referenz-Konsument: Das wasm wird **lazy
geladen**, erst wenn der Nutzer eine Verbindungs-Oberfläche erreicht, sodass
Landing-Pages die rund 43 MB nie zahlen, und es gibt ein `DeviceRemote` pro
Tab. Die Exporte sind global für die Seite, `init` sitzt also hinter einem
Singleton.

Die Browser-Erweiterung packt dasselbe wasm ein, instanziiert es heute aber
nicht: Sie treibt die Konnektivität über den Plattform-Proxy plus die
eigenen Proxy-APIs des Browsers und konsumiert den **anderen**
Einstiegspunkt des npm-Pakets, `@urnetwork/sdk-js/react` — schlichte
`fetch`-basierte API-Hooks und generierte Typen, die kein wasm brauchen.
Wenn du nur die REST-Fläche willst, importiere die und ruf `init` nie auf:
Der Loader löst die wasm-URL zur Laufzeit auf statt über ein statisches
`new URL(..., import.meta.url)`, Bundler emittieren sie also nicht für
Konsumenten, die sie nicht brauchen.

## cgo

### Die Daemon-Aufteilung

Die ausgelieferten URnetwork-Apps für Linux und Windows sind auf dieser
Schicht gebaut, und beide nutzen dasselbe Zwei-Prozess-Muster — die Form,
die für jede cgo-Integration zu kopieren ist, die ein tun-Device berührt.
Das SDK liefert die Engine und die C-API; der Daemon ist deiner, mit diesen
beiden veröffentlichten Apps als quelloffenen Referenzen:

- Ein **Root-/Dienst-Prozess** (systemd-Unit, Windows-Dienst) führt das
  `DeviceLocal` aus und besitzt das tun-Interface.
- Der **unprivilegierte UI-Prozess** führt ein `DeviceRemote` aus und hängt
  sich über Loopback-mTLS-Device-RPC an; die Standardadresse des SDK ist
  `127.0.0.1:12025`.
- Die mTLS-PEMs werden der UI über einen Kanal übergeben, den das OS selbst
  autorisiert: einen **per `SO_PEERCRED` geprüften Unix-Socket** auf Linux
  (geprüft beim Accept, bevor irgendein Frame gelesen wird), eine **Named
  Pipe** auf Windows. Dieser Handshake ist die echte Autorisierungsgrenze,
  nicht der TCP-Port; Loopback-mTLS hält nur andere lokale Nutzer vom Port
  fern.

### RAII-Wrapper-Semantik (`urnetwork_sdk.hpp`)

- Handles sind in besitzende Typen gewickelt, deren Destruktor
  `urnet_release` aufruft. Aber **Release ist nicht Close/Stop**: Den
  letzten Wrapper fallen zu lassen stoppt kein Device und schließt keine
  Verbindung. Ruf zuerst `*_close` / `*_stop` auf (im Wrapper `.close()` /
  `.stop()`) und lass den Wrapper dann freigeben. Mehrere Handles können auf
  ein lebendes Objekt zeigen, ein Scope-Ende darf also nie still eine
  lebende Sitzung töten.
- Subscriptions kommen als `urnet::Sub` zurück; der Destruktor meldet ab.
- Fehler treten als `urnet::Error`-Exceptions auf, die den
  `out_error`-Text tragen.

Eine harte Regel, das Gegenteil dessen, was Leute erwarten:

```cpp
sub = device.addConnectChangeListener([&](bool enabled) { /* ... */ });
sub.close();   // returns immediately; a callback may still be running
// do NOT free what that lambda captured here
```

Listener-Listen sind Copy-on-Write, das Abmelden entfernt den Eintrag also
und kehrt zurück. Das ist Absicht (ein Callback *darf* sich aus sich selbst
heraus entfernen), aber es heißt, dass ein Callback auf einem anderen Thread
noch laufen kann, nachdem das `Sub` weg ist. Der echte Absturz ist, den
Zustand zu zerstören, den dieser Listener eingefangen hat; halt ihn über den
Abbau hinaus am Leben, mit einem `shared_ptr` oder einem Flag, das der
Callback prüft.

Für Leak-Disziplin nimmst du vor einem Szenario einen Snapshot von
`urnet_live_handle_count()`, fährst Konstruieren/Nutzen/Schließen/Freigeben
durch und assertierst, dass er zur Baseline zurückkehrt. `cgo/smoke` tut
genau das und ist das Muster zum Kopieren.

### Was tatsächlich gebunden ist

Die C-ABI wird aus der Go-Fläche *generiert*, und der Generator schreibt
`cgo/coverage_report.txt`: jedes exportierte Symbol, und für jedes, das
nicht hinüberkam, den Grund (Go-Contexts, `net.Conn`-Interna,
Funktionsparameter, Aufrufe mit Pool-Besitz, RPC-gob-Typen). Lies diese
Datei, nicht den Go-Quellcode, um „Ist das aus C aufrufbar?“ zu beantworten.
Ein neuer Go-Export erreicht den Header erst, wenn der Generator läuft, der
Header wird also neben der Bibliothek ausgeliefert, mit der er geschnitten
wurde. Pinne sie zusammen.

### Wire-Kompatibilität

Das Device-RPC-Wire-Protokoll ist **versionsgepinnt (`DeviceRpcVersion`,
derzeit 1) und wird durchgesetzt**, vom Local bei jedem Sync. Es ist bewusst
*nicht* die Release-Version: Die gehosteten Hälften werden unabhängig
deployt, sie zu koppeln würde also nach jedem Server-Deploy jeden Browser
ablehnen. Eine Nichtübereinstimmung wirft nicht. Das Remote bleibt
unsynchronisiert und versucht es erneut, was identisch aussieht zu „der
Daemon läuft nicht“. Unterscheide beides mit `GetRemoteConnected()` plus
`GetSyncError()`: Ein leerer Sync-Fehler heißt noch nicht erreichbar,
während `"device rpc version mismatch: ..."` oder `"device instance
mismatch: ..."` eine Ablehnung ist, die keine Wiederverbindung je behebt.
Liefere Daemon und UI aus demselben SDK-Release.

## Querschnittsthemen

### Einstellungen, die Richtlinien tragen

Diese Flags halten Produktentscheidungen, kein Tuning, und ihre
Standardwerte sind die Privacy-Haltung, die deine Nutzer bekommen. Was ein
Device ab Werk ausliefert:

| Einstellung | Standard | Wirkung | Hauptkosten |
|---|---|---|---|
| `SetPerformanceProfile` | nil (auto) | Qualitäts- und Geschwindigkeitsfenster laufen nebeneinander; Datenverkehr tritt über mehrere Provider zugleich aus (üblicherweise 3–8), mit Bindung pro Website | einen `WindowType` festzupinnen verengt auf ein Fenster |
| `AllowDirect` | aus | hält den Anonymisierungs-Hop, sodass kein Provider die echte IP des Nutzers sieht | an: mehr Durchsatz, und der Provider sieht die echte IP des Nutzers |
| `PostQuantumEncryption` | an | versiegelt die Sitzung Client↔Provider, sodass der Operator Bytes weiterleitet, die er nicht lesen kann | ein Provider, mit dem nicht versiegelt werden kann, wird übersprungen, nicht unversiegelt genutzt |
| `SetRouteLocal` | erlauben | Datenverkehr fällt auf die lokale Route zurück, wenn der Tunnel abreißt | Verbieten ist der Kill Switch: Der Datenverkehr stoppt stattdessen |
| `SetProvideMode` | none | das Device bietet dem Netzwerk keine Kapazität an | Bereitstellen als public oder network lässt fremden Datenverkehr über die IP des Nutzers austreten |

Vorbehalte, die zur Tabelle gehören:

- `AllowDirect` ist die Geschwindigkeitseinstellung, und die IP, die es
  offenlegt, ist genau der Hop, den es entfernt. Die Apps zeigen es
  invertiert als „Starke Anonymisierung“ (Strong Anonymization),
  standardmäßig an, und es wird auf gehosteten Device-Profilen erzwungen
  ausgeschaltet, was auch immer ein Aufrufer setzt. Präsentiere es deinen
  Nutzern nie als geschenkte Geschwindigkeit; präsentiere es als den Tausch,
  der es ist.
- `PostQuantumEncryption` ist die Ende-zu-Ende-Sitzung Client↔Provider, die
  die Apps als „Post-Quanten-Verschlüsselung“ (Post Quantum Encryption)
  ausliefern, ab Start standardmäßig an und provider-bereit: Jeder aktuelle
  Provider-Build aktiviert die Responder-Seite. Solange es an ist, arbeitet
  der Client fail-closed — er trägt keine Anwendungsdaten im Klartext, und
  einen Provider, mit dem er nicht versiegeln kann, überspringt er, statt
  ihn unversiegelt zu nutzen. Die Kosten liegen bei der Verfügbarkeit, nicht
  bei der Vertraulichkeit. Schalt das Flag aus, und der Datenverkehr kann
  wieder den Standardpfad nehmen.
- `SetRouteLocal` ist das Kill-Switch-Primitiv („lokalen Datenverkehr
  erlauben“). Schalt es aus und gib deinen Nutzern den Schalter, den jede
  URnetwork-App hat, damit Datenverkehr stoppt, statt zurückzufallen, wenn
  der Tunnel abreißt.

Die Blindheit des Providers gegenüber der Identität deines Nutzers ist
bedingungslos; die Blindheit des Operators gegenüber Inhalten ist es
standardmäßig ebenfalls. Zwei Eigenschaften, beide wahr für ein Profil ab
Werk — und genau das macht es zutreffend zu sagen, dass keine einzelne
Partei zugleich die Identität deines Nutzers und seine Aktivität hält. Kipp
eines der beiden Flags, und du gibst die zugehörige Eigenschaft weg, statt
sie zu verdienen.

Die drei Modi, die diese beiden Flags auswählen, formuliert für die Nutzer,
die du routest:

| Modus | Operator sieht | Provider sieht | Wie du ihn bekommst |
|---|---|---|---|
| **Relay, versiegelt** | Konto/Quellverbindung, Provider-Zuordnung, Chiffretext sowie Timing/Volumen | Ziel-Datenverkehr und eine Geräte-/Vertrags-ID, **nicht** die echte IP des Nutzers | der Standard: `PostQuantumEncryption` an, `AllowDirect` aus |
| **Relay, Standard** | Konto/Quellverbindung, Provider-Zuordnung, innere Ziele und Paketbytes | Ziel-Datenverkehr und eine Geräte-/Vertrags-ID, **nicht** die echte IP des Nutzers | nur mit `PostQuantumEncryption` aus |
| **Direkt** | weniger Relay-Beteiligung | **die echte IP des Nutzers** und Ziel-Datenverkehr | opt-in: `AllowDirect` an (auf gehosteten Profilen erzwungen aus) |

Der Standard liefert die erste Zeile bereits aus, ohne dass etwas zu setzen
wäre. Für die zweite muss `PostQuantumEncryption` aus sein: Solange es an
ist, überspringt der Client einen Provider, mit dem er nicht versiegeln
kann, statt auf diese Zeile zu fallen. Es gibt keine API pro Verbindung, die
meldet, auf welcher Zeile eine bestimmte Sitzung gelandet ist. Das
[Bedrohungsmodell](/docs/threat-model) spielt diese Zeilen gegen benannte
Angreifer durch und benennt ausdrücklich, wo jede versagt.

### Threading-Regeln

- Nicht-View-Objekte (`DeviceLocal`, `DeviceRemote`, `Api`, Network Spaces)
  sind nebenläufigkeitssicher. Ruf sie aus jedem Thread auf.
- **View-Controller sind Single-Threaded**, sofern einer nichts anderes
  dokumentiert. Treib jeden aus einem Thread (normalerweise deinem
  UI-Thread). Browser-JavaScript erfüllt das umsonst; auf Android, Apple und
  cgo beißt es, wo Callbacks auf Go-verwalteten Threads eintreffen.
- Callbacks feuern auf beliebigen SDK-verwalteten Threads. Marshalle auf
  deinen UI-Thread, bevor du UI anfasst, und blockiere nie in einem: Ein
  `DeviceRemote` serialisiert seine Callbacks durch einen einzelnen
  gepufferten Channel, ein langsamer Listener erzeugt also Gegendruck auf
  jede Subscription darauf.

### Artefaktgrößen

Plane Download- und Packaging-Budgets um diese herum:

| Artefakt | Größe (komprimiert, wo vermerkt) |
| --- | --- |
| Android AAR | ~37 MB |
| Apple xcframework (zip) | ~116 MB |
| JS wasm | ~43,5 MB |
| Linux c-shared (zip) | ~25 MB |
| Windows c-shared (zip) | ~21 MB |

### API-Stabilität

Versionen sind datumsbasiert (`vYYYY.M.D-<code>`); einen SemVer-Vertrag gibt
es noch nicht, und das npm-Paket ist Beta und wird nächtlich neu
veröffentlicht. Behandle alle drei Flächen als in Bewegung, bis eine 1.0
kommt: Pinne exakte Versionen, lies bei jedem Sprung die Release Notes, und
halte gepaarte Artefakte (Daemon und UI, Erweiterung und App) auf einem
Release.
