Hier starten

Bedrohungsmodell

66 Min. LesezeitAls Markdown ansehen ↗

Dieses Dokument benennt URnetworks Angreifer und sagt, was das System gegen jeden einzelnen tut und was nicht. Es ist geschrieben, um angegriffen zu werden. Wo eine Eigenschaft an eine Bedingung geknüpft ist, steht die Bedingung im selben Satz; wo etwas eine Designabsicht ist, die der ausgelieferte Code noch nicht durchsetzt, ist es als Designabsicht gekennzeichnet und nicht als Eigenschaft. Eine Zusammenfassung für Leser, die die vollständige Akte nicht lesen werden, steht vor Abschnitt 1.

Der Maßstab ist Tors eigene Erklärung der Angriffe, die Onion-Routing nicht abwehrt: Ein Angreifer, der beide Enden eines Circuits beobachtet, kann sie über das Timing korrelieren, und Tor sagt das öffentlich. URnetwork liefert keinen Cover-Traffic und kein Padding aus, hier gilt also dasselbe, und Abschnitt 8 sagt es ohne Abmilderung.

Stand der unabhängigen Prüfung, einmal genannt und für alles Folgende gültig. Es gibt kein unabhängiges Audit des Protokolls, der Connect-Engine oder des Server-Codes des Operators — der Schicht, auf der die Privacy-Zusagen liegen. Zwei externe Prüfungen aus 2025 decken andere Flächen ab: ein unabhängiger externer Penetrationstest der Web-Anwendung und der API (25. April – 5. Mai 2025, mit Zugangsdaten, manuell, OWASP Top 10 plus eine Prüfung der ASVS-Kontrollen) und eine MASA-AL2-Prüfung der Android-App durch die Leviathan Security Group (abgeschlossen am 23. Mai 2025), die bestanden wurde und von der Leviathan selbst sagt, sie sei „keine ganzheitliche Sicherheitsbewertung oder umfassender Penetrationstest“. Keine der beiden untersuchte Logging, Aufbewahrung, den Datenpfad oder das Protokoll. Jede Behauptung in diesem Dokument ist deshalb eine Behauptung über lesbaren Quellcode, den jeder prüfen kann und den außerhalb des Projekts noch niemand als Ganzes geprüft hat.

Wie die Quellenangaben zu lesen sind

Behauptungen zitieren path:line gegen die Arbeitsbäume unter /Users/brien/urnetwork/{connect,sdk,server,proxy,extension}, Stand 2026-09-17, als jede zeilengenaue Quellenangabe in diesem Dokument anhand ihres Bezeichners neu gegen die Bäume aufgelöst wurde. Zeilennummern verschieben sich; die Bezeichner nicht. Wo ein Befund in einem früheren Code-Verifikationsdurchgang festgestellt wurde, zitiert er review/verified/ARCHITECTURE.md oder review/verified/PRIVACY-ENFORCEMENT.md, die derselben Konvention folgen.

Diese Neuauflösung war nicht kosmetisch, und sie verdient es, als Warnung für alle festgehalten zu werden, die eine ältere Fassung lesen. Zwischen dem Durchgang vom 2026-08-07 und diesem hat sich connect/transfer.go in der Länge ungefähr verdreifacht, und fast jede connect-Quellenangabe hat sich verschoben — das Fail-Closed-Gate beim Senden, zitiert bei transfer.go:2796, steht heute bei :6751, und das Gate beim Empfangen, zitiert bei :5844, steht bei :14886. Behandle jede Zeilennummer in einer Fassung dieses Dokuments, die das obige Datum nicht trägt, als unzuverlässig, und navigiere anhand der Bezeichner.

Drei Kennzeichnungen werden bewusst verwendet:

  • Eigenschaft — der Code setzt sie durch, und ein Angreifer, der die anderen Parteien kontrolliert, kann sie trotzdem nicht verletzen.
  • Disziplin — der Code entscheidet sich, etwas nicht zu tun, wozu er in der Lage wäre. Eine Deployment-Änderung oder ein einzeiliger Patch könnte das umkehren.
  • Designabsicht — das Design nennt dies als Ziel; der ausgelieferte Code setzt es noch nicht durch.

Zusammenfassung

Dieser Abschnitt ist für Leser, die die vollständige Akte nicht lesen werden. Jeder Satz darin ist durch einen nummerierten Abschnitt weiter unten oder durch die Aussage zur unabhängigen Prüfung oben gedeckt.

Was das System ist. URnetwork ist ein Privacy-Netzwerk, in dem Mitglieder Datenverkehr für andere Mitglieder weiterleiten. Der Pfad ist du → Extender → Operator → Provider → Internet. Die Vertrauensaufteilung liegt zwischen zwei Relay-Parteien: dem Operator, der weiß, wer du bist, und dem Provider, der sieht, wohin dein Datenverkehr geht.

Die Frage, die dieses Dokument beantworten soll. Können ein nicht vertrauenswürdiger Operator und eine nicht vertrauenswürdige Menge von Providern trotzdem eine private und anonyme Sitzung tragen, solange sie nicht kolludieren? Die ehrliche Antwort lautet heute zum größten Teil, und die Lücke lässt sich genau verorten: Die Blindheit des Providers für deine Identität hält gegen einen beliebig feindseligen Provider, bedingungslos; die Blindheit des Operators für Inhalte hält gegen einen passiven Operator und gegen jeden Angreifer auf dem Pfad, ruht aber darauf, dass der Operator die Identitätsschlüssel der Provider ehrlich verteilt und deine Provider nicht in feindseliger Absicht auswählt. §1.1 ist das Vertrauensmodell, das dies Partei für Partei darlegt; §7 ist der Mechanismus hinter dem Vorbehalt.

Die zwei Eigenschaften, mit je eigenem Geltungsbereich. Auf einem Relay-Pfad erhält der Provider nie deine echte Quell-IP; keine Einstellung und kein Provider-Build ändert das (§2). Der Operator kann die versiegelte Sitzung zwischen Client und Provider nicht lesen, die auf den fünf nativen Apps (Android, iOS, macOS, Windows und Linux) unter dem Namen Post-Quanten-Verschlüsselung (Post Quantum Encryption) verfügbar ist (§2.1). Die Browser-Erweiterung hat keine versiegelte Sitzung — ihr Client-Gerät läuft im Inneren des Operators, der als Protokoll-Übersetzungspunkt wirkt, eine Versiegelung kann dort also nicht existieren (§2.4).

Drei Vertrauensabhängigkeiten, und wo jede steht. Die Versiegelung fiel früher in jedem Modus still offen aus (fail open). Behoben am 2026-08-08/10: Mit eingeschalteter Post-Quanten-Verschlüsselung läuft der Client fail-closed und weigert sich, Anwendungsdaten im Klartext zu senden oder anzunehmen, statt darauf herabzustufen (§2.2). Zwei Dinge bleiben, und beide sind Vertrauen in den Operator statt in die Kryptografie:

  1. Schlüsselverteilung. Kann der Operator einen Provider-Schlüssel austauschen? Seit 2026-09-17 erheblich schwerer, und nicht mehr abstreitbar. Mit eingeschalteter Post-Quanten-Verschlüsselung hält der Client den Sitzungs-Cipher zurück, bis der vom Vertrag gelieferte Provider-Schlüssel gegen eine signierte, per Hash verkettete, an die Domain gebundene Registrierungshistorie bestätigt ist, die der Operator veröffentlicht hat, und eine verifizierte Abweichung beendet die Sitzung für diesen Provider (§7.4). Ein Operator, der austauscht, muss den Austausch jetzt signieren und hinterlässt damit einen dauerhaften, zurechenbaren Datensatz, der von dem abweicht, was jeder andere Leser dieser Client-ID sieht. Was das nicht aufhält, ist ein Operator, der ab deinem ersten Kontakt feindselig ist und dabei konsistent bleibt: Er signiert eine einzige schlüssige Kette, die seinen eigenen Schlüssel nennt, und ein Client ohne unabhängige Sicht darauf, welcher Signierer maßgeblich ist, kann das nicht erkennen. Signiert heißt nicht: für den Signierer unfälschbar.
  2. Provider-Auswahl. Der Operator sortiert die Provider-Auswahl und gibt sie zurück, und der Client hat keine Möglichkeit zu prüfen, ob die ihm angebotenen Provider voneinander oder vom Operator unabhängig sind (§5, §6.1). Ein nicht vertrauenswürdiger Operator muss also nicht mit Providern kolludieren; er kann seine eigenen auswählen.
  3. Und der Standardwert. Die Fail-Closed-Garantie ist an einen Schalter gebunden, der in allen fünf nativen Apps weiterhin ausgeschaltet ausgeliefert wird (§2.2), ein Client in Standardkonfiguration öffnet also überhaupt keine Sitzung zum Provider: Jedes Anwendungspaket passiert den Operator unversiegelt, auf dem Standardpfad.

Die Punkte 7, 8 und 9 von verify/BEFORELAUNCH.md verfolgen alle drei; §2.2, §6 und §7.4 beschreiben den aktuellen Stand.

Wogegen das System nicht schützt. Ein Angreifer, der sowohl dein Zugangsnetz als auch die Provider beobachtet, die deinen Datenverkehr tragen, kann die beiden Enden über Größe und Timing korrelieren (§8.1). Ein globaler passiver Beobachter liegt vollständig außerhalb des Geltungsbereichs (§8.3). URnetwork liefert kein Padding, keinen Cover-Traffic und kein Mixing aus; Abschnitt 11 ist die vollständige Liste.

Unabhängige Prüfung. Es gibt kein unabhängiges Audit des Protokolls, der Connect-Engine oder des Server-Codes des Operators; zwei externe Prüfungen aus 2025 decken andere Flächen ab: ein Penetrationstest der Web-Anwendung und der API und eine MASA-AL2-Prüfung der Android-App durch die Leviathan Security Group, die bestanden wurde.

Wo die Details stehen. Abschnitt 1 definiert die Parteien und legt das Vertrauensmodell Partei für Partei dar (§1.1). Abschnitt 2 behandelt die drei Verbindungsmodi und was jede Partei in jedem von ihnen sieht. Die Abschnitte 3 bis 5 nehmen sich jeden Angreifer der Reihe nach vor: einen feindseligen Provider, einen Netzbeobachter und den Operator. Die Abschnitte 6 und 7 behandeln vom Operator betriebene Provider, Kollusion und den Austausch von Provider-Schlüsseln. Die Abschnitte 8 bis 12 behandeln Verkehrskorrelation, Gerätekennungen, rechtliche Anordnungen, die Liste dessen, wogegen das System nicht schützt, und was dieses Dokument nicht klären konnte. Abschnitt 13 sagt, wohin Schwachstellen und Korrekturen zu melden sind.

1. Die Parteien

ParteiBetrieben vonPosition
ClientDem NutzerDie App- oder SDK-Instanz; auf dem Browser- und dem Proxy-Pfad läuft sie stattdessen auf den Servern des Operators (§2.4)
Ingress-LBDem Operatornginx; stempelt die beobachtete Client-Adresse in X-UR-Forwarded-For (xops/gitops-unused/gitops-prod/urnetwork/connect/ingress.yaml:9 — das einzige Ingress-Manifest im Baum, und sein Verzeichnis heißt gitops-unused, es beschreibt also womöglich nicht die Produktion; §12)
Connect-DienstDem OperatorEin oder zwei Prozesse auf verschiedenen Hosts, verbunden über eine interne Exchange-Verbindung (server/connect/resident.go:395). Nicht „ein Relay-Server“
API / SteuerungsebeneDem OperatorAuth, Provider-Discovery und -Ranking, Verträge, das Nachschlagen öffentlicher Schlüssel (server/api/api.go)
ProviderJedem MitgliedEmpfängt weitergeleitete Pakete und wählt das Ziel über die eigene Verbindung an (connect/ip.go:7167 NewRemoteUserNatProvider; Receive bei :8726, ReceiveBatch bei :8486 → LocalUserNat, :731,784)
ExtenderJedem FreiwilligenDie erste Etappe des Pfades: ein TLS-Weiterleitungs-Relay auf einer unabhängigen Adresse. Trägt die Sitzung Client↔Plattform, ohne sie zu terminieren, sieht also die verbindende IP und kann nicht entschlüsseln — der innere Stream ist „das eigene TLS des Clients zum Ziel, in das der Extender nie hineinsieht“ (connect/extender/extender.go:35-44; connect/net_extender.go:34-48)
DoH-ResolverCloudflare, Google, Quad9, OpenDNSDer App-Pfad löst DNS über HTTPS durch den Tunnel zu einem dieser vier auf (connect/net_http_doh.go:148-166)
ZahlungsdienstleisterStripe, Apple, Google, Solana/CircleHalten die echte Identität bezahlter Konten; der Operator speichert die Verknüpfungsschlüssel (server/db_migrations.go:1804, :4732, :2441)

Der Operator ist BringYour, Inc., eine C-Corporation aus Delaware mit einer Anschrift für Mitteilungen in San Francisco (docs/legal/terms.md:8-9,265-271 nennt die Gesellschaft und die Anschrift; die Inkorporation wird in den veröffentlichten Bedingungen nicht genannt). Abschnitt 10 behandelt, was das für Rechtsverfahren bedeutet.

Eine Anmerkung zum Vokabular, denn die ökonomische Schicht des Netzwerks verwendet für dieselbe Partei ein anderes Wort. Provider heißen Miner, wo sie für die Bandbreite bezahlt werden, die sie tragen, und das Netzwerk ist darauf ausgelegt, viele unabhängig betriebene Operatoren zu haben statt eines einzigen. Keins von beidem ändert die Kryptografie: Das Muster ist immer ein Client ↔ ein Operator ↔ ein Provider, und jede Eigenschaft weiter unten ist eine Eigenschaft dieses Dreiecks. Was eine Vielzahl von Operatoren und Providern ändert, ist die Plausibilität der Annahme in §1.1, dass nicht kolludiert wird, nicht der Mechanismus, den diese Annahme schützt.

1.1 Das Vertrauensmodell

Lies diese Tabelle so: Wenn diese Partei beliebig feindselig ist und die anderen ehrlich sind, überlebt die Eigenschaft dann?

Eigenschaftgegen einen feindseligen Providergegen einen feindseligen Extendergegen einen feindseligen Operator
Deine Identität ist vor dem Exit verborgen (der Provider erfährt nie deine Adresse oder dein Konto)Ja — bedingungslos. Die Adresse ist nie auf dem Draht; der Einstiegspunkt des Providers nimmt nur IDs entgegen (§2, §3)Ja. Ein Extender sieht deine Adresse, die dein Internetanbieter ohnehin kennt, und erfährt nichts über den Exit. Die Ausnahme ist die Provider-Rolle, die du einschaltest, indem du deine Verbindung teilst: Sie weist sich gegenüber den Extendern, die sie misst, mit ihrer Client-ID aus (§4)Nein. Der Operator weiß konstruktionsbedingt, wer du bist; das ist seine Rolle (§5)
Deine Ziele sind vor dem Koordinator verborgen (der Operator kann die Sitzung nicht lesen)entfällt — der Provider ist die Partei, die die Ziele siehtJa. Der Extender leitet eine Sitzung weiter, die er nicht terminieren kann (§4)Bedingt. Hält gegen einen passiven Operator, gegen Downgrade, gegen einen Operator, der später feindselig wird, und gegen einen, der zwischen seinen eigenen Kanälen inkonsistent ist. Hält nicht gegen einen Operator, der ab dem ersten Kontakt feindselig und in sich konsistent ist (§7.4)
Kein Downgrade auf Klartext (Datenverkehr ist versiegelt oder fließt nicht)Ja, bei eingeschaltetem Schalter. Das Gate beim Empfangen weist Klartext ab, dessen Hülle abgestreift wurde (§2.2)Ja, bei eingeschaltetem SchalterJa, bei eingeschaltetem Schalter — Weglassen wird zur Verweigerung, nicht zur Offenlegung. Nein, bei ausgeschaltetem, und das ist weiterhin der ausgelieferte Standard (§2.2)
Es existiert kein Browserverlauf, der erzwungen oder geleakt werden könnteJaJaJa — strukturell. Nirgends im Transferregister existiert ein Feld für Ziel, Host, URL, SNI, Port oder Domain (§5, §10)
Dein Datenverkehr ist Ende zu Ende nicht korrelierbarNeinNeinNein. Kein Padding, kein Cover-Traffic, kein Mixing — bewusst außerhalb des Geltungsbereichs (§8)

Daraus folgen drei Dinge, und sie sind die ehrliche Gestalt der Antwort auf die Frage aus der Zusammenfassung.

Dem Provider wird nichts anvertraut, und das ist strukturell durchgesetzt. Nicht durch eine Richtlinie, nicht dadurch, dass der Provider den offiziellen Build ausführt — sondern dadurch, dass die Adresse nie gesendet wird. Das ist die stärkste Eigenschaft in diesem Dokument.

Dem Operator werden zwei Dinge anvertraut, und beide sind offen. Er verteilt die Identitätsschlüssel der Provider, gegen die die Versiegelung geprüft wird (§7), und er wählt aus, welche Provider dir angeboten werden (§5, §6.1). Keines von beidem kann der Client heute verifizieren.

Diese beiden sind nicht unabhängig voneinander, und das zweite ist das schärfere. Ein Dokument, das sagt „sicher, solange Operator und Provider nicht kolludieren“, spielt das Problem herunter, solange der Operator die Provider aussucht: Er muss niemanden anstiften, wenn er auswählen kann, wen er ohnehin kontrolliert. Die Unterscheidung im Design lautet Austausch gegenüber Auswahl (connect/DESIGNNOTES2.md §3.4). Für das Schließen des Austauschs gibt es eine kryptografische Antwort, und eine wird gerade gebaut (§7.4). Für das Schließen der Auswahl braucht es etwas, das der Client über die Unabhängigkeit einer Provider-Menge prüfen kann, und nichts im ausgelieferten Code versucht das.

2. Die drei Modi und was jede Partei sieht

Diese Tabelle ist maßgeblich und stimmt mit OVERVIEW.md überein. Alles Weitere in diesem Dokument ist der Mechanismus dahinter.

ModusOperator siehtProvider siehtStandard und Verfügbarkeit
Relay, versiegeltKonto/Quellverbindung, Provider-Zuordnung, Chiffretext sowie Timing/VolumenZiel-Datenverkehr, Geräte-/Vertrags-ID, nicht deine echte Quell-IPnur native Apps, und heute opt-in: als Standard beschlossen, wird aber weiterhin ausgeschaltet ausgeliefert; siehe §2.2
Relay, StandardKonto/Quellverbindung, Provider-Zuordnung, innere Ziele und PaketbytesZiel-Datenverkehr, Geräte-/Vertrags-ID, nicht deine echte Quell-IPheute der native Standard, da die Versiegelung ausgeschaltet ausgeliefert wird (§2.2), sowie die Browser- und Proxy-Pfade (§2.4); ist sie an, wird ein Provider, zu dem sie sich nicht aufbauen lässt, übersprungen statt hier bedient (§2.2)
Direktweniger Relay-Beteiligungdeine echte Quell-IP und Ziel-Datenverkehropt-in, durch Ausschalten von Starke Anonymisierung (Strong Anonymization, §2.3)

Es folgen zwei asymmetrische Aussagen, und sie sind nicht gleich stark:

Die Blindheit des Providers für deine Identität ist eine Eigenschaft, und sie ist auf beiden Relay-Pfaden bedingungslos. Der eingehende Einstiegspunkt des Providers nimmt IDs entgegen, nie eine Adresse: RemoteUserNatProvider.Receive ist über einen TransferPath aus drei 16-Byte-IDs parametrisiert (connect/ip.go:8726, Batch-Form bei :8486; connect/connect.go:50). Kein Transport-Protobuf trägt eine Client-Adresse, eine Stadt oder ein Land — ein Grep über connect/protocol/*.proto nach location|city|country|region liefert nichts (review/verified/ARCHITECTURE.md §1.3). Es gibt keine Einstellung, die das auf einem Relay-Pfad abschaltet, und kein Provider-Build kann eine Adresse erlangen, die ihm nie gesendet wird.

Die Blindheit des Operators für Inhalte ist eine Einstellung, noch kein Standardwert — über einem Mechanismus mit nur noch einem Mangel. Der Mechanismus hat einen Namen: Post-Quanten-Verschlüsselung, ein Schalter im Connect-Drawer der Android-, iOS-, macOS-, Windows- und Linux-Apps, der eine Ende-zu-Ende-Sitzung zwischen deinem Client und dem Provider versiegelt (§2.1). Die erste Bedingung ist, dass der Nutzer den Schalter findet, denn er wird weiterhin ausgeschaltet ausgeliefert (siehe unten). Seit 2026-08-10 ist nicht mehr bedingt, ob die Versiegelung still nicht zustande kam: Ist der Schalter an, trägt eine Verbindung, die sich nicht versiegeln lässt, überhaupt keinen Anwendungsverkehr, statt ihn im Klartext zu tragen (§2.2). Bedingt bleibt, ob der Operator, der die Provider-Schlüssel ausliefert, gegen die die Versiegelung geprüft wird, sie ehrlich ausliefert (§7.4). Lies das, bevor du dich auf diese Eigenschaft verlässt; für den Nutzer ist es nicht sichtbar.

Standard beim Launch: AN beschlossen; Stand 2026-09-24 weiterhin AUS ausgeliefert. Der Product Owner hat am 2026-08-07 entschieden, dass die versiegelte Sitzung eingeschaltet ausgeliefert wird, damit die Blindheit des Operators der Standard wäre und nicht eine Entscheidung, die der Nutzer treffen muss. Das ist nicht umgesetzt. Das Flag des Performance-Profils steht in allen fünf Apps weiterhin auf false — android/.../PerformanceProfileSettings.kt:34,88, apple/app/network/Shared/ViewModels/DeviceManager.swift:395,479, windows/app/src/App/SdkHost.h:192 und linux/app/src/ConnectDrawer.cpp:746 —, und das SDK startet ein Gerät ganz ohne Profil. Ohne das Flag behalten die Fenster-Clients DefaultEncryptionSettings mit dem Modus EncryptionModeOff (connect/transfer_encrypt.go:768-770; sdk/device_local.go:4483), ein Client in Standardkonfiguration läuft heute also weder in Required noch in Opportunistic: Er öffnet keine Sitzung zum Provider, und jedes Anwendungspaket passiert den Operator unversiegelt (verify/BEFORELAUNCH.md Punkt 7, Urteil: nicht bestanden). Jede Aussage in diesem Dokument über Fail-Closed-Verhalten gilt nur bei eingeschaltetem Schalter, und ein Leser sollte davon ausgehen, dass er aus ist, sofern er ihn nicht selbst eingeschaltet hat.

Diese Lücke wiegt in eine Richtung schwerer als früher. Weil das Fail-Closed-Gate an denselben Schalter gebunden ist, würde das Einschalten des Standards eine echte Garantie an jeden Nutzer verbreiten, statt bloß die Reichweite eines stillen Fehlschlags zu vergrößern — das Gegenteil dessen, wovor dieser Abschnitt warnte, als die Versiegelung noch in jedem Modus offen ausfallen konnte. Was der Standardwert weiterhin nicht beheben kann, ist §7.4, denn das ist eine Eigenschaft der Versiegelung selbst: Eine standardmäßig eingeschaltete Versiegelung, in die sich der Operator als MITM einklinken kann, schützt nur gegen einen passiven Operator.

Die Browser-Erweiterung hat überhaupt keine versiegelte Sitzung (extension/src/utils/auth-params.ts:33, performance_profile: null). Das ist Geltungsbereich, kein Standardwert, und die Launch-Entscheidung ändert daran nichts.

Überall dort, wo die Versiegelung fehlt — die Browser- und Proxy-Endpunkte (§2.4) und die nativen Apps mit ausgeschalteter Versiegelung (§2.2) —, terminiert der Operator das TLS/QUIC des Clients und leitet IP-Pakete im Klartext weiter (connect/protocol/ip.proto:9-16). Er entscheidet sich dafür, nur den Routing-Header zu parsen — FilteredTransferFrame enthält nichts als den Transferpfad (connect/protocol/transfer.proto:85-87), und das Relay deserialisiert genau diese Nachricht und nichts darüber hinaus (server/connect/resident.go:3824) —, und das ist Disziplin, keine kryptografische Barriere.

2.1 Was die Versiegelung genau ist

Eine TLS-1.3-Sitzung direkt zwischen Client und Provider, als gewöhnliche Control-Frames durch denselben Operator getragen: EncryptedControl-Nachrichten enthalten die Handshake-Bytes und laufen im normalen zuverlässigen, geordneten Pack/Ack-Fluss mit, eine Sitzung pro Peer-Paar, wobei die lexikografisch kleinere ClientId die Rolle des TLS-Clients übernimmt (connect/transfer_encrypt.go:30-60). Der Schlüsselaustausch ist das hybride X25519MLKEM768, mit konventionellem X25519 als Fallback (connect/transfer_encrypt.go:376-384); das AEAD ist AES-256-GCM über einem 32-Byte-Schlüssel, exportiert unter dem RFC-5705-Label urnetwork-connect-aead, mit einer zufälligen 12-Byte-Nonce pro Nachricht (:88-99 für die Labels und Längen, :283-301 für die Konstruktion); Identitäten sind Ed25519, im Prozess von ClientKeyManager erzeugt (connect/transfer_key.go:87-143, der einzige Aufruf von ed25519.GenerateKey in den drei Bäumen, bei :113) — „Post-Quanten“ bezieht sich also allein auf den Schlüsselaustausch. Wechselseitiges TLS ist Pflicht (ClientAuth: tls.RequireAnyClientCert, connect/transfer_encrypt.go:406), und das Peer-Zertifikat wird auf der Sequence-Schicht gegen das Commitment des Vertrags geprüft statt vom TLS-Stack; der Kommentar bei connect/transfer_encrypt.go:386-398 erklärt, dass die Seite in der TLS-Server-Rolle ohne mTLS keine PeerCertificates hätte und die Vertragsprüfung für die Hälfte aller Peer-Paare scheitern würde.

2.2 Fail-closed, wenn du es verlangst, fail-open, wenn nicht

Aktualisiert am 2026-08-10. Dieser Abschnitt sagte früher, die Versiegelung falle still offen aus und es gebe keine Fail-Closed-Option. Das trifft nicht mehr zu, und die Änderung ist die bedeutendste Härtung in der Geschichte dieses Dokuments — sie hat den Downgrade-Vektor in beide Richtungen geschlossen. Was folgt, ist das aktuelle Verhalten, aus dem Quellcode gelesen.

Verschlüsselung ist weiterhin eine binäre Eigenschaft von Cipher() != nil, und ein Cipher von nil bedeutet weiterhin, dass die Sitzung nicht nutzbar ist: Ein gescheiterter Handshake, ein gescheiterter Identitätsnachweis oder ein Vertrag, der den öffentlichen Schlüssel des Peers nie getragen hat, lassen ihn allesamt nil. Der strukturelle Grund verdient es, einmal genannt zu werden, denn alles andere hängt daran — das AEAD existiert, bevor der Peer authentifiziert ist, und wird bewusst zurückgehalten. completeHandshake parkt es auf derivedTlsCipher, statt es verfügbar zu machen (connect/transfer_encrypt.go:1719-1726), und nur maybeVerifyPendingPeerIdentityProof stuft eine Epoche zu „etabliert“ hoch, und das nur bei einem Nachweis, der verifiziert (:1928-2050, Hochstufung bei :1979-1981). Cipher() liest establishedEpoch (:2625), für jeden Aufrufer ist ein nicht nachgewiesener Peer also nicht von einem unvollständigen Handshake zu unterscheiden.

Eine Korrektur an früheren Fassungen dieses Abschnitts: Ein gescheiterter Identitätsnachweis lässt die Sitzung nicht mehr bloß „unauthentifiziert“ zurück. Er setzt jetzt außerdem identityFailedTerminal, bricht die Epoche ab und löst ein EncryptionEventIdentityFailed aus (connect/transfer_encrypt.go:2005-2030). Die Log-Zeile lautet weiterhin „session left unauthenticated“ (:2027), und der Wortlaut untertreibt, was der Code tut.

Was sich geändert hat, ist, was danach passiert. Es gibt jetzt drei Modi (connect/transfer_encrypt.go, EncryptionMode):

ModusVerhalten
EncryptionModeOffDer Nullwert. Sitzungsschicht inaktiv, alles Klartext.
EncryptionModeOpportunisticVersiegelt, sobald eine Sitzung zustande kommt, bis dahin Klartext — oder dauerhaft, falls sie nie zustande kommt. Das historische Verhalten.
EncryptionModeRequiredGibt Anwendungsdaten niemals im Klartext an einen Peer weiter, für den eine Sitzung erwartet wird, und nimmt sie von ihm auch nie im Klartext an.

Unter EncryptionModeRequired wird die Garantie an vier Stellen durchgesetzt:

  • Eingangs-Gate beim Senden (connect/transfer.go:6751-6800, in SendSequence.Pack). Ein Anwendungspaket wartet innerhalb des Zeitlimits des Aufrufers auf den Cipher und wird dann abgelehnt, ungesendet — nie herabgestuft — mit einem typisierten ErrEncryptionRequiredNotEstablished (connect/transfer_encrypt.go:505). Das Gate sitzt bewusst vor der Vergabe einer Sequenznummer: Der Handshake in der Client-Rolle läuft über dieselbe Sequenz, ein bereits nummeriertes Frame zurückzuhalten würde also eine Lücke in die geordnete Empfangsseite reißen und das ClientHello hinter dieser Lücke festhalten — ein Deadlock genau des Handshakes, der das Gate freigeben würde.
  • Absicherung beim Senden (connect/transfer.go:11726-11738, in writeMaybeWrappedBytes). Ein Frame, das den Writer ohne Cipher erreicht, wird abgelehnt statt geschrieben; das deckt das enge Zeitfenster ab, in dem eine Sitzung zwischen Einreihen und Schreiben abgebaut wird.
  • Gate beim Empfangen (connect/transfer.go:14886-14913). Ein Anwendungs-Frame im Klartext von einem Peer, für den eine Sitzung erwartet wird, wird verworfen und auditiert — das ist die Hälfte, auf die es am meisten ankommt, denn sie schließt das Downgrade, bei dem ein Angreifer die Hülle abstreift und der Empfänger den Klartext sonst annehmen würde. Das Frame wird bestätigt (ack) und verworfen, statt unbestätigt zu bleiben, denn ein zurückgehaltenes Ack würde eine Lücke in die geordnete Sequenz reißen und beide Seiten festfahren.
  • Vorfilter für Kandidaten (connect/ip_remote_multi_client.go:11695-11710, EncryptionCapabilityPrefilter, standardmäßig true bei :196). Ein Fenster-Kandidat, von dem die Out-of-Band-Schlüssel-API der Plattform sagt, er habe nie einen Identitätsschlüssel veröffentlicht, wird sofort als gescheitert gewertet, denn er kann den Handshake nie abschließen. Die Ablehnungsregel ist bewusst eng gefasst (:13229-13236): Ein Fehler beim Abruf führt nicht zur Ablehnung, die Unerreichbarkeit des Operators lässt sich also nicht in eine Sperre von Providern verwandeln. Er beschleunigt nur ein ohnehin sicheres Scheitern; er lässt nie einen Kandidaten zu.

Handshake-Control-Frames, Acks und Control-Plane-Peers sind bewusst ausgenommen — das Gate deckt die Anwendungs-Nutzlast ab, nicht das Gerüst, das sie erst in Gang bringt.

Welchen Modus du bekommst, entscheidet der Schalter Post-Quanten-Verschlüsselung. Ist er an, läuft der Consumer-Client in EncryptionModeRequired (connect/ip_remote_multi_client.go:13080-13091, aus dem Profil-Flag bei :1311): Ein Provider, der keine Sitzung aufbauen kann, trägt überhaupt keinen Anwendungsverkehr für dich, statt ihn im Klartext zu tragen. Der genannte Preis ist Verfügbarkeit, bewusst in Kauf genommen. Provider laufen in EncryptionModeOpportunistic (sdk/device_local_provider.go:186), damit ein Provider sowohl versiegelte als auch unversiegelte Consumer bedienen kann; das ist eine Kompatibilitätsentscheidung auf der Responder-Seite und schwächt die Garantie des Initiators nicht.

Beachte, dass der Nullwert des Modus-Enums Off ist (connect/transfer_encrypt.go:469-504), ein Einstellungs-Struct mit Nullwerten verschlüsselt also nichts, statt still halb zu verschlüsseln. Das ist eine bewusste Entscheidung, getroffen, als Encrypt bool durch den Tri-State ersetzt wurde.

Was das für den Schalter bedeutet. Mit eingeschalteter PQE fällt die Versiegelung nicht mehr offen aus: Sie fällt geschlossen aus, laut, beim Senden wie beim Empfangen. Mit ausgeschalteter PQE — so werden die Apps ausgeliefert — ist der Client nicht einmal opportunistisch: Er läuft in EncryptionModeOff und startet nie eine Sitzung, sein gesamter Anwendungsverkehr fließt daher unversiegelt, und nichts sagt es dem Nutzer. Die ehrliche Aussage ist also an eine Bedingung geknüpft, und der Standardwert ist entscheidend: siehe den Hinweis Standard beim Launch in §2.

Das Verhalten ist durch Tests abgedeckt, nicht nur durch Kommentare — TestRequiredEncryptionFailsClosedAgainstPlaintextPeer, TestRequiredGateNonBlockingSendRefusesPreCipher, TestRequiredGateBoundedBudgetRefusesUnsent, TestRequiredSendRefusalTypedErrorAndEvent und TestRequiredContractFreeWithoutKeySourceFailsClosed.

Ein veralteter Kommentar, den man beim Audit ignorieren sollte: Der Kommentar innerhalb von completeHandshake bei connect/transfer_encrypt.go:1723-1725 behauptet weiterhin uneingeschränkt, dass bis zur Verifikation des Identitätsnachweises „der Wrap-Pfad cipher == nil sieht und der Datenverkehr im Klartext fließt“. Das stimmt nur im Modus Off und Opportunistic; unter Required setzen die Gates oben es außer Kraft. Der Kommentar ist älter als die Korrektur.

Seitdem teilweise geschlossen: Es gibt jetzt eine Identitätsfläche, wenn auch kein Banner pro Verbindung. Das SDK exportiert die Menge der Peers mit einer etablierten, identitätsverifizierten Sitzung, zusammen mit einem kanonischen Anzeige-Fingerabdruck — sdk/post_quantum_identity.go definiert PublicIdentityKeyHash als „DER kanonische Anzeige-Hash für Identitätsschlüssel auf jeder Plattform: der SHA256 des Schlüssels, kodiert als ungepaddetes Base32 in Großbuchstaben (RFC 4648)“, mit ProviderIdentity bei :24-30 und einem View-Controller bei sdk/post_quantum_identity_view_controller.go:21, genutzt von beiden Apps (PostQuantumIdentityViewModel.kt, PostQuantumIdentityStore.swift). Das ist mehr als ein Hook: Es ist der Anfang eines vom Nutzer prüfbaren, vom Operator unabhängigen Vergleichskanals, denn ein anderswo beschaffter Fingerabdruck lässt sich mit dem angezeigten vergleichen. Was weiterhin fehlt, ist eine schlichte Anzeige pro Verbindung: „diese Sitzung ist versiegelt“. Die verbleibenden Signale im Gerätelog sind peer identity proof verified — cipher is now usable (connect/transfer_encrypt.go:2004) bei Erfolg, ein Errorf bei Fehlschlag (:2026-2029) und ein NotifyRequiredSendBlocked-Ereignis, wenn das Gate ablehnt.

2.3 Der Direktmodus

Starke Anonymisierung auszuschalten setzt AllowDirect, dessen eigener Feldkommentar lautet: // setting this to true exposes the real source IP to the provider (sdk/sdk.go:1130-1139). Der Flow wird auf einen P2P-Stream gezwungen (connect/ip_remote_multi_client_probe.go:1207-1209), über einen WebRTC/ICE-Datenkanal (connect/transport_p2p_webrtc.go:818-825), und ICE heißt, dass beide Endpunkte die Adressen des jeweils anderen erfahren. Der Standard ist aus — ein Nil-Performance-Profil ergibt false (connect/ip_remote_multi_client.go:841-850; sdk/sdk.go:1130-1139).

Zwei erzwungene Fälle: Peers im selben Netzwerk (deine eigenen Geräte) erlauben direkt immer (connect/ip_remote_multi_client.go:843-850,2656-2660), und gehostete Geräte erzwingen aus (sdk/device_local.go:608-615). Beachte, dass der Direktmodus den Operator nur aus dem Datenpfad entfernt. Verträge, Provider-Auswahl und Steuernachrichten laufen weiterhin über die Plattform, der Operator erfährt also weiterhin, welchen Provider du genutzt hast und wie viele Bytes bewegt wurden.

2.4 Der Pfad, den die Tabelle nicht abdeckt: die Browser- und die Proxy-Endpunkte

In der Browser-Erweiterung, der Web-App und an den HTTPS-/SOCKS5-/WireGuard-Endpunkten betreibt der Operator den Client. Die Erweiterung stellt ein serverseitiges Proxy-Gerät bereit (extension/src/utils/auth-params.ts:22-36, enable_socks/enable_http), und der Browser wird auf den Proxy-Endpunkt des Operators gerichtet — standardmäßig HTTPS CONNECT (extension/src/bridge/background.ts:220; extension/src/utils/proxy-manager.ts:264). Die SDK-Instanz, die Verträge öffnet und das Provider-Fenster hält, läuft in server/proxy, nicht auf der Maschine des Nutzers.

Deshalb kann auf diesen Plattformen keine versiegelte Sitzung existieren, und zwar aus Gründen der Architektur, nicht wegen noch nicht aufgewendeter Entwicklungsarbeit. Die versiegelte Sitzung läuft Client↔Provider, und ihre Zusage hängt daran, dass der Client-Endpunkt auf dem eigenen Gerät des Nutzers lebt. Ein Browser oder ein einfacher Proxy-Client spricht sein eigenes Protokoll (HTTPS CONNECT, SOCKS5, WireGuard) und kann die Engine des Netzwerks nicht ausführen, also betreibt der Operator das Gerät des Clients aus der Ferne und wirkt als Protokoll-Übersetzungspunkt zwischen beiden. Eine von diesem entfernten Gerät ausgehandelte Versiegelung begänne im Inneren des Operators — genau der Partei, die die Versiegelung blind machen soll. Die Implementierung hat diesen Tausch bewusst in Kauf genommen: Diese Flächen existieren der Bequemlichkeit halber auf Plattformen und Protokollen, die den vollen Client nicht beherbergen können, und die Docs dürfen sie nicht als versiegelt beschreiben.

Die Folgen, klar benannt:

  • Die Blindheit des Providers für deine Identität gilt weiterhin — der Provider erhält weiterhin nur IDs.
  • Die Blindheit des Operators existiert auf diesem Pfad in keiner Form. Der Operator terminiert die Proxy-Verbindung, erhält den Zielhost aus CONNECT und hält die Schlüssel des Clients.
  • Das gehostete Gerät installiert keinen DNS-Upgrade-Mux (server/proxy/proxy_device.go:858, SetUpgradeMuxSettings(nil)), das DoH im Tunnel des App-Pfades (§3.2) gilt hier also nicht.

Was auf diesem Pfad an die Stelle der Verschlüsselung tritt, ist Speicherdisziplin: Der Proxy-Datenpfad loggt auf clientgesteuerten Pfaden nichts, und ein Regressionstest nagelt das fest — proxy/socks5_nolog_test.go, TestClientDrivenTrafficNeverLogs, sichert null Log-Zeilen über fehlerhafte, übergroße und nicht anwählbare Fälle zu. Zwei ehrliche Vorbehalte: Das erklärte Motiv des Tests ist Log-Amplification-DoS und nicht Privatsphäre, und der Panic-Recovery-Handler loggt sehr wohl eine Client-Adresse (proxy/socks5_server.go:129-132). Siehe review/verified/PRIVACY-ENFORCEMENT.md §2.2.

3. Angreifer: ein feindseliger Provider

Im Geltungsbereich und vorausgesetzt. Bereitstellen kann jeder. Es gibt für die Teilnahme als Provider keine Attestierung, keinen Stake, keine Identitätsprüfung und keine Sybil-Abwehr — eine Suche über die Bäume server, connect und sdk nach sybil|kyc|attestation liefert nichts Relevantes. Ein Provider führt Open-Source-Code auf Hardware aus, die er kontrolliert, geh also davon aus, dass er einen veränderten Build ausführt.

Was er sieht. Alles, was ein Internetanbieter für den Datenverkehr sieht, den er trägt: Ziel-IP und -Port, Paketgrößen und Timing sowie TLS-SNI, wo der Client es im Klartext sendet. Er empfängt rohe IP-Pakete und wählt sie selbst an (connect/protocol/ip.proto:9-16; connect/ip.go LocalUserNat). Er sieht außerdem die client_id, die als SourceId des Vertrags mitgeführt wird (connect/protocol/transfer.pb.go:1779; server/controller/connect_controller.go:816-830) — eine Kennung pro Fensterplatz, die nur der Operator zu einem Konto auflösen kann, und nie die device_id, die auf dem Draht kein Feld hat. Ihre Lebensdauer und die etlichen Dinge, die sehr wohl über Sitzungen hinweg bestehen bleiben, stehen in §9.

Was er tun kann.

  • HTTP im Klartext: lesen und verändern. Port 80 wird standardmäßig unverändert an den Ausgang durchgereicht — HttpUpgradeUnencrypted ist der Standardmodus (connect/ip_mux_upgrade.go:38-47). Für jeden Datenverkehr, der nicht selbst verschlüsselt ist, steht ein Provider in derselben Position wie ein feindseliger WLAN-Hotspot. HTTPS schützt Anwendungsinhalte vor dem Provider; nichts in URnetwork legt eine zweite Schicht über das eigene TLS des Ziels.
  • Datenverkehr selektiv verwerfen, verzögern oder anhalten. Ein Provider, der Datenverkehr quittiert, aber keinen zurückgibt, wird als Blackhole markiert und entfernt (connect/ip_remote_multi_client.go:37-54), Verweigerung wird also erkannt und umfahren — aber die Erkennung ist statistisch, und der Datenverkehr, den er vor der Entfernung gesehen hat, war trotzdem gesehen.
  • Über die Leistung lügen, um Datenverkehr anzuziehen. Das Ranking nutzt gemessene Latenz und gemessenen Durchsatz (server/model/network_client_location_model.go:4098,4220), und gemessen wird beim Operator, es ist keine Selbstauskunft des Providers, das ist also begrenzt — aber es ist ein Eingang ins Ranking, keine Integritätskontrolle.
  • Flows innerhalb der eigenen Sicht korrelieren. Die Bindung pro Website pinnt eine Website auf einen Provider (connect/ip_remote_multi_client.go:837), ein einzelner Provider sieht also für die Websites, die er hält, einen zusammenhängenden Ausschnitt des Surfens eines Clients.

Was er ohne Hilfe nicht tun kann. Deine echte IP auf einem Relay-Pfad erfahren, dein Konto oder deine E-Mail-Adresse erfahren oder eine client_id einer Person zuordnen. Dafür braucht es den Operator (§5).

3.1 Was ip_security einschränkt und was nicht

Die ip_security-Schicht der Connect-Engine läuft am eigenen Ausgang des Providers und auf dem eigenen Gerät des Nutzers, nie zentral beim Operator — die Felder IngressSecurityPolicyGenerator/EgressSecurityPolicyGenerator des Operators existieren, werden aber nie zugewiesen oder gelesen (server/connect/resident.go:263-264; review/verified/PRIVACY-ENFORCEMENT.md §4.1). Sie blockiert BitTorrent- und Filesharing-Signaturen, opake nicht standardisierte Protokolle, in Reputationslisten geführte Ziele und Datenverkehr mit Angriffsmustern, und sie tut das auf bewusst dünner Grundlage: 5-Tupel plus ein Payload-Präfix, begrenzt auf 8 Pakete / 512 Bytes.

Die Reputationstabellen werden erzeugt, nicht von Hand gepflegt. connect/security und connect/blocker fassen öffentliche Threat-Intelligence-Feeds — abuse.ch Feodo Tracker (Botnetz-C2), Spamhaus DROP und DROPv6, Emerging Threats (kompromittierte IPs), Blocklist.de, CINS Army, TweetFeed, ViriBack, BruteForceBlocker — zu gepackten Bereichstabellen zusammen (46.789 IPv4-Bereiche im Snapshot vom 2026-08-05; der Header von connect/ip_security_cfaa_block.go führt die Feed-Liste und die Namensnennung). Die Release-Pipeline erzeugt beide Tabellen neu aus den Live-Feeds (build/all/run.sh, der Schritt CONNECT_IP_UPDATE), jedes Release liefert also einen aktuellen Snapshot aus, und ein aktueller Client blockiert den zuletzt gelisteten Adressraum. Der Snapshot altert mit dem installierten Build; die Listen aktualisieren sich pro Release, nicht over the air.

Die Grenze des Payload-Präfixes steht in connect/ip_security_dmca.go:136, wobei der Servername aus dem Flow-Schlüssel gelöscht wird (:366,386,424) und Zähler auf (version, protocol, port) geführt werden, wobei das IP-Feld nie aktiviert ist (connect/ip_security.go:371).

Sie ist eine Schutzmaßnahme für ehrliche Provider, keine Sicherheitsmaßnahme gegen feindselige. Sie begrenzt, was die Verbindung eines Providers nach außen tragen wird. Gegen das, was ein Provider mit dem Datenverkehr tut, den er sehr wohl trägt, richtet sie nichts aus, sie läuft in einem Prozess, der dem Provider gehört, und ein veränderter Build kann sie abschalten. Lies sie nicht als Schranke für einen feindseligen Provider.

Ein Meldedetail gehört hierher, weil es ein Metadatenkanal ist. Ein Treffer auf eine BitTorrent-Signatur gibt Incident zurück, was ReportAbuse aufruft (connect/ip.go:9014,9110) und einen PeerAudit-Control-Frame an den Operator sendet, der die Geräte-ID des Peers und einen Boolean trägt — kein Ziel, keine Domain, keine Inhalte (connect/transfer.go:2971). Der Operator hat heute keinen Handler für diesen Nachrichtentyp, beim Empfang wird also nichts gespeichert (server/controller/connect_controller.go:132,436-437; ein Grep über das gesamte Repo nach PeerAudit in server liefert null Treffer). Das ist eine Disziplin, die eine einzige case-Anweisung von der Änderung entfernt ist. Verwürfe opak verschlüsselten Datenverkehrs sind still, ohne Meldung (connect/ip_security_dmca.go:483-486).

3.2 DNS

Auf dem App- und SDK-Pfad wird einfaches DNS auf UDP/TCP 53 abgefangen und durch den Tunnel über DoH aufgelöst (connect/ip_mux_upgrade.go:120-148, standardmäßig installiert bei sdk/device_local.go:7105), zu Cloudflare, Google, Quad9 oder OpenDNS (connect/net_http_doh.go:150-155). Ein Provider sieht daher eine HTTPS-Verbindung zu einem öffentlichen Resolver, nicht die Anfrage. Zwei ehrliche Grenzen:

  • Ein DNS-Leck-Fenster beim Start ist im Quellcode dokumentiert. Solange das DoH im Tunnel noch aufgebaut wird, wird eine Anfrage im Wettlauf gegen einen Resolver über den Ausgang des lokalen Hosts geschickt, „um den Preis eines kurzen DNS-Lecks beim Start“ (connect/ip_mux_upgrade.go:120-126,78-92). Dein lokales Netzwerk und dein Internetanbieter können diese Anfragen sehen.
  • Der WireGuard-Endpunkt gibt dem Client einen festen öffentlichen Resolver mit, DNS = 1.1.1.1 (server/model/network_client_proxy_model.go:774), und Port 53 wird von der Sicherheitsrichtlinie ungeprüft durchgelassen (connect/ip_security_cfaa.go:113). Auf diesem Pfad durchqueren DNS-Anfragen den Provider als einfaches DNS, und der Provider kann sie lesen und beantworten.

Die vier DoH-Resolver sind Dritte, die Anfragen von der Ausgangsadresse eines Providers eintreffen sehen, ohne Verknüpfung zu deinem Konto. Das ist eine echte Abhängigkeit, und sie lässt sich nicht auf null konfigurieren: Irgendjemand muss DNS beantworten.

4. Angreifer: ein Netzbeobachter

Vier Positionen, von der schwächsten zur stärksten.

Dein lokales Netzwerk und dein Internetanbieter. Sie sehen, dass du dich über TLS/QUIC mit connect.bringyour.com verbindest oder mit einem Extender, dazu Größen und Zeitpunkte. Ziele im Inneren des Tunnels sehen sie nicht, außer im oben dokumentierten DNS-Fenster beim Start. Wo die Plattform blockiert ist, treten Extender als gewöhnliche Dienste auf glaubwürdigen Ports auf, und Clients rotieren Personas mit zufälliger Fragmentierung (connect/net_extender.go:123-133; gebaut bei connect/net_extender_network.go:915), und für Netze, aus denen nur DNS herauskommt, gibt es einen DNS-förmigen Transport (connect/transport_pt.go:37,73). Das sind Mechanismen zum Überwinden lokaler und regionaler Firewalls, im Rang unter den direkten Transporten (connect/transport.go:29-33). Sie sind keine Abwehr gegen Verkehrsanalyse und sollten nie als solche beschrieben werden.

Wer einen Extender betreibt. Betreiben kann ihn jeder, die App akzeptiert eine von Hand eingegebene IP, und standardmäßig ist keine Signatur nötig (connect/extender/extender.go:35-44). Ein Extender ist der TCP-Peer, sieht also die IP des sich verbindenden Nutzers, und er kann nicht entschlüsseln, was er weiterleitet, weil die TLS-Sitzung durch ihn hindurch zur Plattform läuft, statt auf ihm zu terminieren (connect/net_extender.go:34-48). Die Extender-Etappe setzt damit einen Beobachter deiner Adresse im ersten Hop in den Pfad, der nicht der Operator ist. Das ist der Tausch, den das Design eingeht, um dich durch eine Sperre zu bringen, und es lohnt sich, über seine Größe genau zu sein: Die Extender-Etappe fügt keine Verschlüsselung und keine Anonymität hinzu — sie erfährt, was dein Internetanbieter ohnehin weiß, und nicht mehr, mit der einen Ausnahme unten. Im ausgelieferten Client werden die direkten Transporte zuerst im Wettlauf probiert, und die Extender-Dialer weiten sich aus, wenn jene scheitern (connect/net_http.go:1686-1709); ein Client, der mit eigenen Extendern konfiguriert ist, nutzt sie als einzige Route (connect/net_http.go:1686-1709).

Die Ausnahme ist die Provider-Rolle. Jeder Client sondiert Extender, um sie nach Umlaufzeit zu ordnen. Ein Gerät attestiert seine Messungen nur, solange es bereitstellt: Die Attestierung beginnt, wenn du das Bereitstellen einschaltest, und endet, wenn du es ausschaltest, auch mitten in einem Sondierungsdurchlauf; ein Gerät, das nie bereitstellt, sondiert anonym (sdk/device_local_provider.go:1155-1167; connect/net_extender_network.go:1240). Solange es bereitstellt, trägt jede Aussage seine Client-ID und ist mit seinem Client-Schlüssel signiert (connect/DESIGNNOTES4.md §1, §3). Der Extender erfährt, dass der Provider mit dieser ID, und damit dieser öffentliche Schlüssel (§9.2), ihn von dieser Adresse aus gemessen hat; der Operator erhält die Provider-ID, den Extender, die Umlaufzeit und den Zeitpunkt. Extender messen einander auf dieselbe Weise, ein Ziel signiert jede Umlaufzeit mit, die es akzeptiert, und der Operator bewahrt die Pings einen Tag lang auf, um daraus Standorte abzuleiten (connect/GEOMAP.md §2). Das sind Betriebsdaten über öffentliche Infrastruktur, unter einem Schlüssel, der in der Provider-Rolle ohnehin dauerhaft und öffentlich ist.

Ein Beobachter eines Endes. Nur die Client-Seite zu beobachten ergibt „dieser Nutzer ist mit URnetwork verbunden“ und ein Byte-/Timing-Profil. Nur den Ausgang eines Providers zu beobachten ergibt Ziele und ein Byte-/Timing-Profil ohne angehängte Identität.

Ein Beobachter beider Enden. Siehe §8. Das ist der Angreifer, den URnetwork nicht abwehrt.

5. Angreifer: der Operator

Der Operator authentifiziert Clients, wählt Provider aus und bewertet sie, verfasst Verträge, verteilt öffentliche Schlüssel und trägt die Pakete auf den Relay-Pfaden. Geh für diesen Abschnitt davon aus, dass er feindselig ist oder gezwungen wird.

Was er ohne jeden zusätzlichen Aufwand sieht: dein Konto; deine Adresse im Moment des Verbindens; eine Stadt/Region/ein Land, aus dieser Adresse per lokalem Datenbank-Lookup abgeleitet — ipDb in server/ip.go öffnet die mitgelieferte Datenbank MaxMind GeoLite2-City (mmdb/geolite2.mmdb) von der Festplatte, und ip.go macht keinerlei Netzwerkaufrufe, eine Adresse wird also nie an einen Geolokalisierungsdienst übergeben — pro Verbindung gespeichert (SetConnectionLocation in server/model/network_client_location_model.go); welche Provider dich bedient haben; Bytezahlen; und auf dem Standardpfad die Zieladressen und Paketbytes im Inneren des Tunnels — obwohl die normalerweise bereits im HTTPS des Ziels selbst stecken.

Was er speichert. Verbindungs- und Auth-Datensätze halten einen Einweg-Hash mit geheimem Schlüssel über den umgebenden /29-Block (IPv4) bzw. /56-Block (IPv6) statt der Adresse (server/ip.go:46-72). Eine Präzisierung, auf die es einem Auditor ankommt: Es ist ein einzelner prozessweiter Pepper aus dem Vault, einmal memoisiert, ohne Salt pro Zeile und ohne jeden Rotationsmechanismus im Repo (server/ip.go:42-43); der IPv4-Schlüsselraum umfasst 2^29 Blöcke, wer den Pepper hält, kann ihn also per Brute Force umkehren. Die Geheimhaltung des Peppers ist die ganze Sicherheitseigenschaft. Das /verify-Subsystem nutzt für IPv6 /48, nicht /56 (server/ip.go:74-89; server/model/verify_model.go:113-116). Der Quellport wird im Klartext neben dem Hash gespeichert (server/db_migrations.go:2044). Die Audit-Zeile bei der Kontoerstellung verzeichnet den gepepperten Hash und den Port, nie das rohe ip:port (server/model/network_model.go:1157), und Audit-Zeilen werden nach 180 Tagen entfernt (server/model/audit_model.go:1018,1052, eingeplant bei server/taskworker/taskworker.go:85-88).

Was gar nicht erst existiert, um gespeichert zu werden. Kein Ziel, kein Host, keine URL, kein SNI, kein Port und keine Domain taucht irgendwo im Transferregister auf — ein Grep der vollständigen Migrationsdatei nach diesen Begriffen liefert null Treffer (review/verified/PRIVACY-ENFORCEMENT.md §1.2). HTTP-Logging läuft durch eine Allowlist aus genau fünf Headern (server/http_log.go:13-29), festgenagelt per Test. Keine Prometheus-Metrik trägt eine IP, einen Host, ein Ziel, einen Port oder eine Client-ID; von 39 deklarierten Metriken sind nur drei mit Labels versehen, und jedes Label ist ein begrenztes Enum (review/verified/PRIVACY-ENFORCEMENT.md §2.5). Log-Uploads aus dem Support-Tab werden nach io.Discard abgeleitet (server/controller/log_file_controller.go:76); nur Metadaten bleiben bestehen.

5.1 Wie lange etwas davon aufbewahrt wird und wohin es geht

Die Aufbewahrung wird von eingeplanten Abräumläufen in server/taskworker durchgesetzt, nicht von einer Richtlinie. Die Fenster unten sind aus den Konstanten abgelesen, die diese Läufe aufrufen, und sie sind kürzer, als die Datenschutzerklärung — die überhaupt keine Aufbewahrungsfrist nennt — einen Leser erwarten ließe.

DatenAufbewahrungsdauerDurchgesetzt bei
Verbindungszeilen und die pro Verbindung daran hängenden Stadt/Region/Land, Latenz und Geschwindigkeit8 Stundentaskworker/work/network_client_work.go:91; das Löschen kaskadiert im selben Statement auf network_client_location (model/network_client_model.go:2580)
Ein Top-Level-Client untätig (kein Auth, kein Connect) → als inaktiv markiert30 TageTopLevelClientIdleExpiration (model/network_client_model.go:2576)
Ein inaktiver Client → harte Löschung, mit Kaskaden+30 TageNetworkClientReapAfterDeactivate (:2556)
Ein ungenutztes Gerät also, Ende zu Ende~60 Tagedie beiden verkettet
Abgeschlossene Verträge7 Tagetaskworker/work/subscription_work.go:21-43
Audit-Zeilen180 Tagemodel/audit_model.go:1018,1052

Zwei Hinweise, die ein Auditor haben sollte. Das Untätigkeitsfenster wurde am 2026-07-18 von 90 Tagen auf 30 verschärft; die Konstante trägt ihre eigene Begründung, und ein veralteter Kommentar bei taskworker/work/network_client_work.go:83-91 sagt noch 90 — das ist die wahrscheinlichste Quelle dieser Zahl, falls sie dir anderswo begegnet. Und RemoveLocationLookupResults ist in jedem Zyklus eingeplant, aber ein No-op — sein Rumpf und sein Model-Aufruf sind beide auskommentiert, und eine solche Tabelle existiert nicht. Es ist ein funktionsloses Überbleibsel, keine Daten, die das Abräumen übersieht; der Standort pro Verbindung wird mit der Verbindungszeile oben gelöscht.

Die Geolokalisierung verlässt die Maschine nie. Der Stadt/Region/Land-Lookup liest die mitgelieferte Datenbank MaxMind GeoLite2-City, mmdb/geolite2.mmdb, von der lokalen Festplatte (ipDb in server/ip.go), und ip.go enthält keinen HTTP-Client und macht keine Netzwerkaufrufe. Der Operator aktualisiert diese Datei mit MaxMinds Client geoipupdate, einem Download, der MaxMind nichts über irgendeinen Nutzer übermittelt. This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com. GeoLite2 enthält Daten von GeoNames, verfügbar unter CC BY 4.0. Keine Adresse wird an einen Geolokalisierungsdienst geschickt, und keine persönlichen Informationen werden an irgendeinen Dritten weitergegeben. Die einzigen Dritten in der Parteien-Tabelle dieses Dokuments halten Daten, die der Nutzer ihnen selbst aushändigt, indem er diesen Weg wählt: die DoH-Resolver, die von den eigenen DNS-Anfragen eines Nutzers durch den Tunnel erreicht werden, und die Zahlungsdienstleister, bei denen sich ein Nutzer registriert, um abgerechnet zu werden.

Der Ingress-LB loggt keine Client-Adressen, und das ist überprüfbar. Die nginx-Konfiguration des Load-Balancers wird von warp erzeugt, das unter Apache 2.0 Open Source ist (warp/LICENSE), das ist also Quellcode, den jeder lesen kann, und keine bloße Behauptung über das Deployment. Zwei Schichten:

  • Das Zugriffslog nutzt ein eigens gebautes Format, das die Adresse weglässt — log_format noclientaddr enthält Zeit, Verbindung, Host, Anfrage, Status, Bytes, Zeitmessungen, Upstream und User-Agent, und die Kommentare der Konfiguration benennen, was bewusst fehlt: $remote_addr und außerdem $http_x_forwarded_for und $http_referer, „die beide eine anderswoher weitergeleitete Client-Adresse tragen können“ (warp/warpctl/config.go:1243-1254).
  • Das Fehlerlog lässt sich nicht formatieren, deshalb wird nginx' eigenes Feld , client: stattdessen nachgelagert entfernt: Der LB führt nginx mit einem stderr aus, das in einen ClientAddrScrubber gewickelt ist (warp/nginx.go:98-103), der dieses Feld per Regex entfernt und danach jedes verbleibende IPv4- oder IPv6-Literal irgendwo im Eintrag durch [scrubbed] ersetzt, wobei der Port erhalten bleibt (warp/nginx.go:111,116,176-232). Nichts ist anhand eines Adressbereichs ausgenommen, also kann auch keine Regel darüber falsch sein, welche Adressen Nutzern gehören.

Ein Regressionstest nagelt das fest: TestNginxLogsOmitClientAddr (warp/warpctl/config_test.go:1208) schlägt fehl, wenn irgendein access_log kein Format nennt, denn nginx' eingebauter Fallback „combined“ beginnt mit $remote_addr.

Die Dienst-Container werden am Deskriptor bereinigt, nicht an der Aufrufstelle. warp führt Dienst-Container mit --log-driver=journald aus (warp/warpctl/run.go:200) und filtert ihre Ausgabe nicht, eine Zeit lang erreichte also jede Zeile, die ein Dienst mit einer Adresse darin schrieb, journald so, wie sie geschrieben war. Die Aufrufstellen waren real, und es waren mehrere: server/http.go:413,453 konstruieren http.Server ohne ErrorLog, die Go-Standardbibliothek schreibt also bei jeder halboffenen Verbindung http: TLS handshake error from — das Loch, das proxy/http.go:199,273 mit ErrorLog: discardLog schließt; vom Client geliefertes SNI wird auf ERROR geloggt (server/tls.go:111); eine rohe Aufruferadresse bei server/proxy/proxy_device.go:858; die WireGuard-Peer-Identität bei server/proxy/server.go:699.

Diese sind nicht mehr die Grenze, denn die Grenze ist nicht mehr die Aufrufstelle. Jeder Dienst ersetzt in der ersten Anweisung von main seine eigenen stdout- und stderr-Dateideskriptoren durch Pipes und bereinigt alles, was in sie geschrieben wird (server.ScrubProcessLogs, server/scrub.go:62, aufgerufen aus allen acht Diensten — server/cli/{api,connect,alt,proxy,taskworker,gossip,monitor,mcp}/main.go). Am Deskriptor statt am Writer anzusetzen heißt, dass es log, glog, den Standard-ErrorLog von net/http, jede Abhängigkeit und jede später hinzugefügte Aufrufstelle abdeckt, ohne dass jemand sie finden muss. Es nutzt dasselbe warp.ScrubAddrs (warp/nginx.go:207), das der LB für nginx nutzt, es gibt also eine einzige Implementierung des Scrubbers statt zweier, die auseinanderdriften können.

Zwei bewusste Grenzen, die ein Auditor beide abwägen sollte, statt sie auf Treu und Glauben hinzunehmen:

  • Crash-Dumps passieren unbereinigt. Eine Zeile mit panic:, fatal error:, signal SIG oder goroutine rastet den Scrubber für den Rest der Lebensdauer dieses Prozesses in den Durchreichmodus ein (scrubPassthroughMarkers, server/scrub.go:48). Ein Dump ist selten und, wenn er auftritt, das Wertvollste im Log, deshalb bleibt er vollständig erhalten — in Kauf genommen, dass ein Stack-Frame oder ein Register eine Adresse tragen kann. Die Verriegelung schreibt beim Auslösen einen sichtbaren Hinweis, ein Prozess, der mit dem Bereinigen aufgehört hat, ist also nichts, worauf ein Operator erst schließen muss.
  • Er bereinigt, was sich als Adresse parsen lässt. Versionsstrings und durch Doppelpunkte getrennte Bezeichner, die sich parsen lassen, werden ebenfalls geschwärzt. Die sichtbarste Folge ist, dass server/monitor/tailer.go, das ip:port aus Log-Zeilen herausliest, jetzt [scrubbed]:443 sieht.

„Ein Dienst schreibt deine Adresse nicht in seine Logs“ ist damit jetzt eine Eigenschaft des Prozesses statt seiner Aufrufstellen. Was das nicht klärt, ist, was journald dann mit dem macht, was es empfängt; §12 hält das als offen fest. Vollständige Aufstellung der ursprünglichen Aufrufstellen: review/verified/PRIVACY-ENFORCEMENT.md §2.4.

Was der Operator kontrolliert und leicht zu übersehen ist. Die Provider-Discovery liegt vollständig auf der Seite des Operators: FindProviders2 ist der einzige aktive Endpunkt (server/api/api.go:140), die Plattform bewertet und sortiert die Kandidaten (server/model/network_client_location_model.go:5056), und der Client nimmt die sortierte Auswahl, die er bekommt. Die einzige Steuerung auf Verbraucherseite ist eine Sperrliste für Standorte (server/api/api.go:149). Ein Client hat keine Möglichkeit zu prüfen, ob die ihm angebotenen Provider voneinander oder vom Operator unabhängig sind.

6. Vom Operator betriebene Provider und Kollusion zwischen Operator und Provider

6.1 Kann der Operator Provider betreiben?

Ja, und nichts im System kennzeichnet oder verhindert das. Bereitstellen steht jedem offen, ohne Attestierung und ohne Stake, und im Code gibt es nirgends ein Konzept von Erstanbieter-, offiziellen oder dem Operator gehörenden Providern — Suchen über server, connect und sdk nach einer solchen Vorstellung liefern nichts. Ein vom Operator betriebener Provider wäre von dem eines Mitglieds nicht zu unterscheiden.

Zusammen mit der Feststellung aus §5, dass der Operator die Provider-Auswahl sortiert und zurückgibt, heißt das: Der Operator kann im Prinzip eigene Provider in das Fenster eines Nutzers setzen. Es gibt keine clientseitige Vielfaltsprüfung, keine Attestierung der Unabhängigkeit und keine veröffentlichte Vermessung der Flotte durch eine außenstehende Partei. Die vorhandenen Gegengewichte sind echt, aber nur teilweise wirksam: Das Fenster hält mehrere Provider zugleich — ein Qualitätsfenster aus 2–6 (harte Obergrenze 12) und ein Geschwindigkeitsfenster aus 1–2 (harte Obergrenze 4), beide aktiv im Standardprofil (connect/ip_remote_multi_client.go:160-189) —, und Provider werden bei ungesunden Statistiken, erkanntem Blackhole, ausbleibendem Ping und der Lebensdauerrotation pro Kanal entfernt (:37-54, :610-616). Das begrenzt, wie viel ein einzelner Provider sieht. Es begrenzt keine Provider-Auswahl, die von einer einzigen Partei getroffen wird.

6.2 Was Operator und Provider gemeinsam rekonstruieren, je Modus

Geh davon aus, dass der Operator und einer oder mehrere Provider in deinem Fenster teilen, was sie jeweils halten.

Relay, StandardRelay, versiegeltDirekt
Deine Identität (Konto, E-Mail/Wallet/Zahlung)OperatorOperatorOperator
Deine echte IP zum VerbindungszeitpunktOperatorOperatorOperator und der Provider direkt
Ziele, die du besucht hastOperator (innere Pakete) und Providernur der ProviderProvider
Verknüpfung zwischen beidemtrivial — eine Partei hält bereits beidesder Vertrag: audit_contract_event paart Client- und Provider-Identität je Vertrag (server/db_migrations.go:325-347)trivial
Ergebnisvollständige Zuordnung des Surfens für alles, was die kolludierenden Provider getragen habenvollständige Zuordnung des Surfens für alles, was die kolludierenden Provider getragen habenvollständige Zuordnung des Surfens, und der Provider kennt deine Adresse ohne fremde Hilfe

Der ehrliche Schluss: Die versiegelte Sitzung schützt nicht gegen Kollusion zwischen Operator und Provider. Sie nimmt dem Operator die eigenständige Fähigkeit, deinen Datenverkehr zu lesen; sie hindert einen Provider, der deine Ziele ohnehin sieht, nicht daran, sie einem Operator zu geben, der ohnehin weiß, wer du bist. Der Vertragsdatensatz, der beides verbindet, ist kein Leck — er ist die Abrechnung, von der das Netzwerk lebt.

Was die Versiegelung gegen diesen Angreifer sehr wohl erkauft, ist eine Begrenzung des Geltungsbereichs: Ist sie an, enthält die eigene Sicht des Operators keine Ziele, Kollusion erfordert also die Mitwirkung genau der Provider, die genau diesen Datenverkehr getragen haben, und zwar zu der Zeit, als er getragen wurde, statt einer Abfrage gegen Datensätze, die der Operator allein hält. Das ist ein bedeutsamer Unterschied im Aufwand und darin, was eine erzwungene Herausgabe rückwirkend erreichen kann (§10). Es ist keine Immunität, und keine Anordnung dreier Parteien, in der eine die anderen auswählt, gewährt Immunität.

Die strukturelle Antwort des Designs, die nicht ausgerollt ist. Das Wire-Protokoll und der Client unterstützen das Verketten weiterer Provider-Zwischenstationen — MaxMultihopLength = 8 (connect/connect.go:15-18), IntermediaryIds an CreateContract (connect/protocol/transfer.proto:547-561), Annahme im Client bei connect/ip_remote_multi_client_api.go:661-662, Server-Verkabelung bei server/controller/connect_controller.go:915. Der aktive Discovery-Endpunkt füllt das Feld nie — FindProvidersProvider wird an genau zwei Stellen konstruiert, und keine davon setzt es (server/model/network_client_location_model.go:5044,5093; das Feld ist bei :3693 deklariert und an keiner der beiden Stellen zugewiesen) —, das ausgelieferte Netzwerk vergibt also Ketten der Länge eins. Provider-Verkettung ist eine Fähigkeit des Protokolls, keine Eigenschaft des Deployments, und die Zahl 8 als Hop-Zahl zu zitieren wäre irreführend.

7. Provider-Identitätsschlüssel und ob der Operator einen austauschen kann

Das ist der Abschnitt, den anzugreifen sich am meisten lohnt, denn die Antwort ist unbequem, und der Quellcode sagt das selbst.

7.1 Ausgabe und Bindung

Jeder connect.Client — ein Provider-Client und jeder der Fenster-Clients des Nutzers — hält ein Ed25519-Schlüsselpaar, das im Prozess von ClientKeyManager erzeugt wird (connect/transfer_key.go:87-143, der einzige Aufruf von ed25519.GenerateKey in den drei Bäumen). Es ist langlebig, wo ein Seed persistiert und wieder geladen wird, was der Provider-Client tut, und pro Prozess frisch, wo keiner persistiert wird, was die Fenster-Clients tun; §9 behandelt den Unterschied und warum er zählt. Die private Hälfte verlässt den Prozess nie; ein Seed falscher Größe ist ein harter Konstruktionsfehler und keine stille neue Identität (:84-94). Die öffentliche Hälfte wird der Plattform in einer ClientKey-Control-Nachricht bekannt gegeben (connect/protocol/transfer.proto:684-700) und serverseitig in Redis unter ckey_ gespeichert, ohne Ablauf — Redis ist die Quelle der Wahrheit, eine SQL-Tabelle gibt es nicht (server/model/network_client_key_model.go:19-65).

Die Bindung ist damit client_id → public key, und der Operator gibt die client_id aus und hält die Zuordnung. Es gibt keinen externen Anker: IDs werden nicht aus Schlüsseln abgeleitet, und es gibt weder ein Transparency Log noch eine DHT. Beide Alternativen sind im Design als erwogen und zurückgestellt vermerkt (connect/DESIGNNOTES.md §3.7, „Deferred alternatives“).

7.2 Rotation und Widerruf

Erneutes Veröffentlichen überschreibt — der eigene Kommentar von SetClientKey lautet „nach client_id geschlüsselt (Rotation überschreibt)“ (server/controller/connect_controller.go:1193-1199). Der Eintrag wird gelöscht, wenn die Client-ID abgeräumt wird (network_client_key_model.go:67-78, aufgerufen aus RemoveDisconnectedNetworkClients). Es gibt keine planmäßige Rotation, keinen Ablauf und keine Widerrufsliste. Ein Provider-Client persistiert sein Schlüsselmaterial und lädt es wieder, seine Identität ist über Neustarts hinweg also stabil (sdk/device_local.go:3592-3597; sdk/local_state.go:788) — und unter Android bewusst auch über eine automatische Abmelde-Bereinigung hinweg stabil (§9.2). Ein Client ohne persistierten Seed erzeugt bei jedem Prozessstart eine neue Identität, und genau das tun die Fenster-Clients des Nutzers. Schlüsselwechsel mitten in der Sitzung werden abgelehnt: SetPeerClientPublicKey gilt nach dem Prinzip erster Schreibzugriff gewinnt, und ein späterer, abweichender Schlüssel wird geloggt und ignoriert (connect/transfer_encrypt.go:2051-2096).

7.3 Verifikation und die zwei Verteidigungen

  • Verteidigung 1, signierte Zertifikatsbindung. Der Provider signiert seine ephemere TLS-Zertifikatskette mit seinem Ed25519-Schlüssel und veröffentlicht beides (connect/protocol/transfer.proto:494-522). Die Plattform hängt die Kette, die Signatur und den öffentlichen Schlüssel des Providers an jeden Vertrag, der diesen Provider benennt (:340-368, :388-406). Der Client lässt die Kette nur zu, wenn die Signatur unter dem öffentlichen Schlüssel des Providers verifiziert, und prüft dann das im Handshake vorgelegte Zertifikat gegen die zugelassene Kette (connect/transfer.go:9560-9583, geprüft bei :11928).
  • Verteidigung 2, Identitätsnachweis im Handshake. Nach dem TLS-Handshake signiert jede Seite die RFC-5705-Exporter-Ausgabe unter dem Label urnetwork-sequence-identity-proof und sendet sie als EncryptedControl (connect/protocol/transfer.proto:615-660,701-712). Das AEAD wird dem Wrap-Pfad vorenthalten, bis der Nachweis des Peers verifiziert (connect/transfer_encrypt.go:1719-1726, :1719-1756), ein MITM, der TLS auf der einen Etappe terminiert und auf der anderen neu handshaked, erzeugt also nicht zusammenpassende Exporter und kann die Signatur nicht fälschen.

Beide Verteidigungen prüfen gegen peerClientPublicKey — und dieser Wert stammt aus dem von der Plattform verfassten Vertrag (connect/transfer.go:9560-9583; connect/transfer_encrypt.go:2051-2096).

7.4 Kann der Operator einen Provider-Schlüssel austauschen? Signiert, zurechenbar und viel schwerer — aber nicht unmöglich.

Aktualisiert am 2026-09-17. In diesem Abschnitt stand früher „Heute: ja.“ Das ist nicht mehr die ganze Antwort, und die Änderung ist nach der Fail-Closed-Korrektur die zweitbedeutendste Härtung in der Geschichte dieses Dokuments.

Der Angriff, in seiner Form unverändert. Ein Operator, der das Zertifikat, die Zertifikatssignatur und destination_client_public_key im Gleichschritt austauscht, überwindet beide Verteidigungen aus §7.3, denn beide prüfen gegen einen Schlüssel, den derselbe Vertrag geliefert hat. Die Design Notes sagen das in den eigenen Worten des Repositorys (connect/DESIGNNOTES.md §3.7).

Was jetzt im Weg steht. Mit eingeschalteter Post-Quanten-Verschlüsselung (EncryptionModeRequired) hält der Client den Sitzungs-Cipher zurück, bis der vom Vertrag gelieferte Identitätsschlüssel gegen eine signierte Registrierungshistorie bestätigt ist, die der Operator veröffentlicht hat, und eine verifizierte Abweichung ist für diesen Provider endgültig — es fließt kein Datenverkehr zu ihm, und er wird aus dem Fenster entfernt.

Jede Registrierung (connect/transfer_key_history.go:200-213) trägt die Deployment-Domain, eine monoton steigende Generation, den Identitätsschlüssel, eine Verknüpfung zum Inhalts-Hash ihrer Vorgängerin und eine secp256k1-Signatur des Root-Signierers des Operators. Der Client prüft die kanonische Kodierung, die Recovery der Signatur, die Lückenlosigkeit der Kette ab Generation 1, Domain und Befugnis des Signierers sowie eine Bewegung nur nach vorn gegenüber einem gepinnten Kettenkopf (VerifyClientKeyHistory, :422). Das Gate sitzt in Cipher() (connect/transfer_encrypt.go:2699-2716), die Entscheidungstabelle in connect/transfer_key_history_session.go:157. Auf der Serverseite speichert server/model/st_client_key_history.go die signierte Kette, server/controller/st_client_key_history_public.go:77 liefert sie aus, geroutet bei server/api/api.go:226.

Der Operator kann also nicht mehr inkonsistent und zugleich abstreitbar sein. Um auszutauschen, muss er sich in signierter Form auf eine Kette festlegen, und dieses Artefakt ist dauerhaft, zurechenbar und weicht von dem ab, was jeder andere Leser dieser Client-ID sieht — einschließlich der unabhängigen Validatoren, die dieses Log bereits auditieren. Die ältere, unsignierte Gegenprüfung gegen GET /key/ läuft weiterhin daneben und ist weiterhin nur beratend; sie loggt CONTRACT vs FETCHED ... MISMATCH ... (today: log only) bei connect/transfer_encrypt.go:2131-2134.

Vier Präzisierungen, die ein Auditor im Kopf behalten sollte.

  1. Signiert heißt nicht: für den Signierer unfälschbar. Ein Operator, der ab deinem ersten Kontakt feindselig und dabei in sich konsistent ist, signiert eine einzige schlüssige Kette, die seinen eigenen Schlüssel nennt. Die Kette zu verifizieren prüft die Kette, nicht die Befugnis dahinter; das Protokoll sagt das selbst — „Der Aufrufer muss den erwarteten Signierer in der gepinnten Kettenversion des Operators gesondert authentifizieren“ (sn/protocol/client_key_history.go:166-167). Der Client antwortet darauf mit im Build gepinnten Signierern plus Trust-on-First-Use (connect/transfer_key_history.go:491), was einen Operator erwischt, der feindselig wird, eine Teilmenge ins Visier nimmt oder einen Fork erzeugt — und keinen, der nie ehrlich war. Das zu schließen erfordert den Abgleich über mehrere unabhängig betriebene Operatoren hinweg, und der ist zurückgestellt (connect/DESIGNNOTES3.md §10).
  2. Weglassen bleibt der billigere Zug, und die Ratsche ist es, die ihn begrenzt. Die Zertifikatsprüfung wird ohne Verriegelung übersprungen, wenn die vertraute Menge leer ist (connect/transfer.go:11915-11926), und der Identitätsnachweis lässt sich überhaupt nicht verifizieren, wenn der Schlüssel des Peers fehlt (connect/transfer_encrypt.go:1947-1950). Die signierte Historie zurückzuhalten ist derselbe Zug eine Ebene höher. Begrenzt wird er durch eine Stufen-Ratsche: Ein Peer, der einmal gegen signierte Belege verifiziert wurde, wird nie wieder ohne sie akzeptiert, und sobald ein Operator einer Installation signierte Belege geliefert hat, wird ein Peer ohne solche abgelehnt (connect/transfer_key_history_session.go:168-185, persistiert in sdk/peer_client_key_pin_store.go). Ein Fehler beim Abruf wird bewusst nicht als Ablehnung behandelt — sonst würde ein einziger API-Ausfall jeden Provider für jeden Nutzer auf einmal ausschließen —, ein Operator, der seinen eigenen Endpunkt kaputt macht, stuft Clients also auf den Vertragsschlüssel herab. Das ist ein bewusst in Kauf genommener Tausch zugunsten der Verfügbarkeit und ein echtes Restrisiko.
  3. All das gilt nur bei eingeschaltetem Schalter. Die Durchsetzung läuft nur unter EncryptionModeRequired (connect/transfer_key_history_session.go:91), denn eine Ablehnung unter Opportunistic stuft auf Klartext herab, und genau das wollte ein austauschender Operator. Da der Schalter weiterhin ausgeschaltet ausgeliefert wird (§2.2), bekommt ein Client in Standardkonfiguration nichts davon.
  4. Auch die Echtheit des Vertrags wurzelt im Operator. Ein Provider verifiziert das HMAC des Vertrags mit einem geheimen Bereitstellungsschlüssel, den der Provider erzeugt und der Plattform bekannt gibt (server/controller/connect_controller.go:727,840; sdk/device_local.go:3592-3597). Der Operator hält eine Kopie, und genau das erlaubt ihm überhaupt erst, Verträge zu verfassen. Das ist von einer Abrechnungsinstanz zu erwarten, aber es heißt, dass „der Vertrag ist echt“ keine vom Operator unabhängige Aussage ist.

Was das untergräbt und was nicht. Es rührt nicht an die Blindheit des Providers für deine Identität, die von keiner Schlüsselverteilung abhängt. Die Blindheit des Operators für Inhalte auf dem versiegelten Pfad ist keine bloße Eigenschaft der Richtlinie mehr: Ein Austausch kostet den Operator jetzt ein signiertes, dauerhaftes, für Dritte erkennbares Artefakt. Sie ist weiterhin keine Eigenschaft, die gegen einen entschlossenen Operator hält, der nie ehrlich war, und jedes URnetwork-Dokument, das die versiegelte Sitzung so beschreibt, als mache sie die Blindheit des Operators bedingungslos, übertreibt. Dieses Dokument ersetzt eine solche Formulierung.

8. Timing- und Verkehrskorrelation und der globale passive Beobachter

8.1 Es gibt keine Abwehr gegen Verkehrsanalyse. Keine.

URnetwork liefert auf Nutzerdaten keinen Cover-Traffic, kein Padding, kein Mixing, keine Batching-Verzögerung und kein Traffic Shaping aus. Das wurde erschöpfend über connect, sdk und server geprüft:

  • Nirgends existiert ein Generator für Chaff, Köder, Dummy- oder Cover-Traffic.
  • Das AEAD ist konstruktionsbedingt längenerhaltend: Die Chiffretextlänge ist Nonce + Klartext + Tag (connect/transfer_encrypt.go:303-336). Kein Protobuf in connect/protocol/ trägt ein Padding-Feld, und das Framing ist ein nacktes 4-Byte-Längenpräfix (connect/message_framer.go:29-31).
  • Das Zusammenfassen auf der Sendeseite ist ausdrücklich verzögerungsfrei: „Es gibt keine Batching-Wartezeit: Die Sequenz nimmt ein zweites Pack nur, wenn es bereits in der Warteschlange steht“ (connect/transfer.go:2971). Jedes jitter in den Bäumen ist Reconnect-Backoff oder Streuung der Kanallebensdauer, kein Traffic Shaping. Die Ack-Kompression (connect/transfer.go:272) verzögert allein Quittungen, um das Nachrichtenvolumen im Relay zu senken.
  • Die einzige Stelle, an der Nutzerdatenverkehr überhaupt getaktet wird, ist die Obergrenze WritePacketsPerSecond des DNS-förmigen Transports (connect/transport_pt.go:68,359-361), die es gibt, um resolverfreundlich zu sein, und die nur für den am wenigsten bevorzugten Transportmodus gilt. Seine „Pump“-Anfragen im Leerlauf haben eine von datentragenden unterscheidbare Länge, er verbirgt also weder Größe noch Rate.

Daher: Ein Angreifer, der den Datenverkehr beobachtet, der in dein Gerät eintritt, und den Datenverkehr, der die Provider verlässt, die ihn tragen, kann beides über Größe und Timing korrelieren. Gegen diesen Angreifer schützt URnetwork nicht. Das ist dieselbe Aussage, die Tor über Ende-zu-Ende-Korrelation trifft, und sie gilt hier mit weniger Spielraum, denn URnetworks Designziel ist niedrige Latenz — und genau diese Eigenschaft macht Korrelation leichter. Ein latenzbegrenzter Pfad aus vier Etappen ist ein bewusster Tausch gegen die zusätzliche Verzögerung eines Mixnets, und das ist die Seite dieses Tauschs, die der Nutzer bezahlt. Die Extender-Etappe mitzuzählen ändert daran nichts: Dieser Hop leitet die verschlüsselte Sitzung an den Operator weiter, ohne sie zu terminieren, er fügt also keine Schicht hinzu, die ein Beobachter abziehen könnte, und keine Verzögerung, in der er dich verlieren könnte.

Zwei Dinge werden bisweilen für Abwehrmaßnahmen gehalten und sind keine. Extender und die geformten Transporte (connect/net_resilient.go:114-215, connect/transport_pt.go:37,73) zielen auf DPI-gestützte Sperren, und die Resilienz-Schicht schaltet sich selbst ab, sobald der Stream steht (net_resilient.go:102,146-161) — sie rührt Records der Datenphase nie an. TLS-Session-Ticket-Caches werden bewusst nicht zwischen Ausgangspfaden geteilt, damit ein Server sie nicht über ein eingelöstes Ticket verknüpfen kann (connect/net_tls.go:38-42,99-103); das ist Unverknüpfbarkeit von Tickets, keine Verkehrsanalyse.

Nichts im Quellcode erkennt Verkehrskorrelation als Einschränkung an: Eine Suche nach traffic analysis, timing correlation, traffic correlation und global adversary über alle drei Bäume liefert null Treffer. Das Bedrohungsmodell im Repo (connect/DESIGNNOTES.md §3.7) dreht sich ausschließlich um den MITM durch den Operator. Dieses Dokument ist die erste Stelle, an der die Lücke aufgeschrieben ist.

8.2 Das Mehr-Provider-Fenster ist kein Mechanismus gegen Korrelation

Der Datenverkehr tritt normalerweise über mehrere Provider zugleich aus — üblicherweise drei bis acht über die beiden Fenster — und die Bindung pro Website hält eine bestimmte Website auf einem Provider (connect/ip_remote_multi_client.go:160-189,837). Das begrenzt tatsächlich, wie viel ein einzelner Exit sieht. Aber die Designbegründung des Fensters im Quellcode ist durchweg Zuverlässigkeit — Abfederung schlechter Ziele, gesundheitsgewichtete Größenanpassung, Blackhole-Erkennung (:27-52) —, und der eine Kommentar, der „geringere Bindung ist privater“ lautet, sitzt auf ClientAffinityTimeout, einem Regler, der auskommentiert ist (:587-591, und noch einmal in den Standardwerten bei :220), während DestinationAffinity aus Gründen der Zuverlässigkeit auf true ausgeliefert wird (:270). Plurale Exit-Identität ist ein echter Vorteil, und es ist fair, ihn zu behaupten; sie als geplante Abwehr gegen Korrelation zu behaupten, wird vom Code nicht gestützt.

8.3 Der globale passive Beobachter: außerhalb des Geltungsbereichs

Ein globaler passiver Angreifer — einer, der einen großen Teil der Internetverbindungen gleichzeitig beobachten kann — liegt außerhalb des Geltungsbereichs, und URnetwork bietet keine Abwehr gegen ihn. Ohne Padding und ohne Cover-Traffic korreliert ein solcher Angreifer Flows über das Relay und die Provider hinweg direkt. Das ist dieselbe Position, die Tor einnimmt, und anders als Tor fügt URnetwork nicht einmal eine Verzögerung pro Hop hinzu, die die Kosten erhöhen würde.

Was wirklich anders ist und sich zu sagen lohnt, ohne es aufzublasen: Die Ausgangsmenge ist eine ständig wechselnde Population unabhängiger Privatanschlüsse und nicht die veröffentlichten Adressbereiche eines Operators, ein Angreifer muss also eine breitere und sich verschiebende Menge von Endpunkten beobachten, um einen einzelnen Nutzer abzudecken, und die Endpunkte lassen sich nicht aus einer Serverliste aufzählen. Das erhöht die Kosten. Es ändert das Ergebnis für einen Angreifer nicht, der beide Enden ohnehin sehen kann.

9. Gerätekennungen und Verknüpfbarkeit

9.1 Was ein Provider über dich beobachten kann

Die SourceId des Vertrags ist eine Client-ID pro Fensterplatz, nicht dein Gerät. StoredContract.SourceId ist die client_id des authentifizierten Aufrufers (server/controller/connect_controller.go:816-830; vom Provider geparst bei connect/transfer.go:6088-6106). Der Generator prägt für jeden Fenstereintrag eine frische Client-ID, ein frisches JWT und eine frische Instanz-ID und entfernt sie beim Abbau — der Quellcode sagt das direkt: „Der API-Generator prägt für jeden Fenstereintrag eine ephemere Plattform-Client-ID (mit frischer Instanz-ID) und entfernt sie beim Abbau“ (connect/ip_remote_multi_client_identity.go:10-15; geprägt über AuthNetworkClient bei connect/ip_remote_multi_client_api.go:691-727, freigegeben über RemoveNetworkClient bei :802-832). Hinzu kommt, dass die Zusammensetzung des Fensters ständig wechselt: Provider scheiden bei ungesunden Statistiken, erkanntem Blackhole, ausbleibendem Ping und der Lebensdauerrotation pro Kanal aus (connect/ip_remote_multi_client.go:37-54), eine Platz-ID ist also konstruktionsbedingt kurzlebig und nicht erst aufgrund einer Richtlinie.

Dein Datenverkehr verteilt sich auf mehrere Provider zugleich, kein einzelner Exit sieht also den Client. Das Standardprofil fährt ein Qualitätsfenster aus sechs und ein Geschwindigkeitsfenster aus einem bis zwei Providern (connect/ip_remote_multi_client.go:160-189). Jeder Provider hält unter seiner eigenen Platz-ID einen Ausschnitt deiner Aktivität, und die Bindung pro Website hält eine bestimmte Website auf einem Provider, statt sie über alle zu verschmieren (:837). Das begrenzt tatsächlich, was ein einzelner ehrlicher, aber neugieriger Exit rekonstruieren kann. Es ist keine Abwehr gegen Korrelation, und §8.2 sagt, warum.

Die Fenster-IDs und die ID deiner Provider-Rolle sind getrennte Identitätsräume, ein Provider kann eine Platz-ID also nicht zu dir zurückverfolgen. Wenn du auch deine Verbindung teilst, ist dein Provider-Client der Top-Level-Client, der den dauerhaften, persistierten Schlüssel-Seed hält (§9.2). Die Fenster-Clients sind andere Clients: gebaut aus einem frischen DefaultClientSettingsWithBufferSize (sdk/device_local.go:4349), das keinen ClientKeySeed trägt, sodass jeder pro Prozess sein eigenes Schlüsselpaar erzeugt, und deine eigene Client-ID ist ausdrücklich aus deiner eigenen Kandidatenmenge ausgeschlossen (sdk/device_local.go:4338). Ein Provider, der eine Platz-ID hält und sie über das nicht authentifizierte GET /key/ nachschlägt, bekommt also einen Schlüssel, der für diesen Prozess frisch ist und mit nichts Dauerhaftem verknüpft ist — nicht mit deiner Provider-Identität, nicht über Neustarts hinweg, nicht über Plätze hinweg. Der dauerhafte, öffentlich auflösbare Schlüssel aus §9.2 gehört zur Provider-Rolle, und nichts auf dem Ausgangspfad legt ihn offen.

Die eigene Client-ID des Geräts erreicht auf dem Ausgangspfad keinen Provider, und die Spur ist kurz genug, um sie zu prüfen. DeviceLocal.sendPacket setzt zwar source := connect.SourceId(self.clientId) — die Top-Level-ID des Geräts — und übergibt sie an den Multi-Client (sdk/device_local.go:4870, Batch-Form bei :4968). Dieser Wert dient nur der lokalen Buchführung: als Schlüssel für das Zusammensetzen von Ausgangsfragmenten und für die Beziehungsprüfung von provideMode (connect/ip_remote_multi_client.go:5876-5906). Das Senden auf den Draht übernimmt der Fenster-Client des jeweiligen Platzes — self.client.SendMultiHopWithTimeoutDetailed(frame, self.args.Destination, …) (connect/ip_remote_multi_client.go:14941-14948), auf einem Client, den der Generator für diesen Platz geprägt hat (:13094) —, und die Quell-ID des Geräts ist nicht unter seinen Argumenten. Ein Grep nach source über diesen Sendebereich liefert nichts.

Die eine dauerhafte Identität ist die Provider-Rolle, und sie zeigt in die andere Richtung. Ein Gerät, das seine Verbindung teilt, führt einen zweiten Client aus, gebaut bei sdk/device_local_provider.go:198 mit ProviderStreamPolicy = true (:191) und dem persistierten Schlüssel-Seed. Netzwerk-Peer-Beziehungen und provider-seitiger Rückverkehr laufen über diesen Client und tragen daher die stabile Top-Level-client_id und den dauerhaften Schlüssel. Das ist die Identität, unter der ein Gerät bedient, nicht die, unter der es surft: Sie liegt gegenüber den Consumern offen, die ein Provider bedient, was §9.2 vollständig dokumentiert, und sie wird nie an die Exits übergeben, die den eigenen Datenverkehr eines Consumers tragen. Die beiden Identitätsräume sind die oben beschriebenen, und sie überschneiden sich nicht.

Aber diese Identitäten werden bewusst bis zu vier Stunden lang wiederverwendet, auf jedem Pfad — nicht nur auf gehosteten. Das Gerät installiert seinen eigenen lokalen Identitätsspeicher, sofern es nicht gehostet ist (sdk/device_local.go:824-830), und eine gespeicherte Fenster-Identität wird gegenüber demselben Ziel erneut abgespielt, bis sie veraltet: windowIdentitiesStaleAfter = 4 * time.Hour (sdk/window_identity_store.go:42-45). Der Zweck ist, die NAT-Flows eines Providers über einen Neustart hinweg am Leben zu halten (connect/ip_remote_multi_client_identity.go:17-23). Der Preis ist, dass ein Provider dieselbe SourceId innerhalb dieses Fensters über App-Neustarts hinweg von dir wieder auftauchen sehen kann. Sag vier Stunden, nicht „ephemer“.

Der Ed25519-Identitätsschlüssel des Clients ist auf dem Ausgangspfad pro Fenster-Client frisch. Die Einstellungen der Fenster-Clients werden aus einem frischen DefaultClientSettingsWithBufferSize gebaut (sdk/device_local.go:7105), das keinen ClientKeySeed trägt (connect/transfer.go:1547-1558), also erzeugt ClientKeyManager ein neues Schlüsselpaar (connect/transfer_key.go:113). Der persistierte Fenster-Snapshot speichert nur Client-ID, JWT und Instanz-ID — keinen Schlüssel-Seed (sdk/window_identity_store.go:42-45), eine wiederhergestellte Identität veröffentlicht also einen neuen Schlüssel. Ist die Post-Quanten-Verschlüsselung aus, wird überhaupt kein Identitätsnachweis vorgelegt (connect/ip_remote_multi_client.go:13080-13091).

Die Quelladresse im Tunnel gilt pro Sitzung und hat wenig Entropie. Das SDK weist seinem TUN eine zufällige RFC1918-Adresse zu — „eine zufällige 10.x.y.h (RFC1918, DHCP-förmig)“ (sdk/device_local.go:7105) —, erzeugt mit einem Host-Oktett in 2..254 über dem kleinsten kollisionsfreien 10.a.b.0/24 (connect/tun.go:517-523). Sie wird im Konstruktor berechnet und nie persistiert, sie ändert sich also mit jeder Sitzung. Der Provider sieht sie sehr wohl: Sie ist das Quellfeld jedes getunnelten Pakets, und der Provider führt seinen NAT-Zustand danach (connect/ip.go:731,784). Rund 253 Werte sind ein schwacher Fingerabdruck pro Sitzung, keine sitzungsübergreifende Kennung.

9.2 Der Identitätsschlüssel der Provider-Rolle ist dauerhaft

Wenn du auch deine Verbindung teilst, hält dein Provider-Client ein langlebiges Ed25519-Schlüsselpaar, dessen Seed im lokalen Speicher persistiert wird (sdk/local_state.go:788, .device_local_key_material, JSON client_key_seed; angewandt bei sdk/device_local_key_material.go:32,90). Anders als die Fenster-Clients bleibt dieser Schlüssel bewusst über eine automatische Abmelde-Bereinigung hinweg erhalten — der Android-Kommentar ist eindeutig: „Einen veralteten oder unvollständigen Auth-Zustand löschen, OHNE die Geräteidentität zu rotieren. Das Identitätsschlüsselmaterial gilt geräteweit, nicht sitzungsweit“ (android/.../MainApplication.kt (clearStaleAuthState)). Token-Erneuerung, Wiederherstellung nach unvollständiger Anmeldung und die erneute Anmeldung nach 30 Tagen Untätigkeit erzeugen daher eine neue client_id und eine neue device_id, während der öffentliche Ed25519-Schlüssel derselbe bleibt. Nur eine ausdrückliche Abmeldung durch den Nutzer rotiert ihn (sdk/local_state.go:788).

Dieser Schlüssel ist veröffentlicht, in jeden Vertrag eingeschlossen, der diesen Client als Ziel benennt (server/controller/connect_controller.go:744-770,816-830), und für jeden lesbar: GET /key/ ist von Haus aus nicht authentifiziert (server/api/api.go:222-223; server/controller/connect_controller.go:1227). Jede Partei — nicht nur ein Provider, den du bedient hast — kann also jede Client-ID zu ihrem öffentlichen Schlüssel auflösen und die Client-IDs zusammenfassen, die sich einen teilen. Für ein Provider-Konto ist das eine dauerhafte Handhabe über Sitzungen, über client_id und über device_id hinweg. Das ist ein echter Preis des Bereitstellens, und er ist nirgendwo sonst im Korpus dokumentiert.

9.3 Beim Operator verknüpft sich alles

  • Eine client_id wird nie widerrufen. Der Kommentar im Code selbst: „client_ids sind global eindeutige Adressen, gleichbedeutend mit IPv6 / sie werden nach der Zuteilung nie widerrufen, um Sicherheits- und Audit-Datensätze zu bewahren“ (server/model/network_client_model.go:74). Ein Top-Level-Client wird nach 30 Tagen Untätigkeit deaktiviert (TopLevelClientIdleExpiration, :2576) und 30 Tage danach hart gelöscht (NetworkClientReapAfterDeactivate, :2556), kaskadierend auf die Gerätezeile und den Redis-ckey (:2537).
  • Eine device_id gilt pro Anmeldung, nicht pro Hardware oder pro Installation: Mit jedem Top-Level-Client wird eine frische geprägt (:332-352), und der Code vermerkt den daraus folgenden „Identitätswechsel (eine frische device_id pro Anmeldung)“ (:2280). Fenster-Clients erben sie (:356-380), und sie wird nie an einen Provider gesendet.
  • Das Netzwerk-Token ist ein JWT mit 24 Stunden Laufzeit (server/jwt/by_jwt.go:55-67,238), vom SDK bei Halbwertszeit erneuert — nach etwa 12 Stunden — mit Jitter (sdk/device_token_manager.go:172-217). Das Token zu erneuern rotiert die Client-ID nicht; derselbe Claim wird neu geprägt (server/model/network_client_model.go:74).
  • audit_contract_event verzeichnet Client- und Provider-Identität je Vertrag (server/db_migrations.go:325-347). client_reliability verbindet den Block-Hash mit geheimem Schlüssel direkt mit einer Client-ID, pro Block: Der ursprüngliche vierspaltige Primärschlüssel (server/db_migrations.go:325-347) wurde später auf (block_number, client_address_hash, client_id) reduziert (:2191-2195), und die aktive partitionierte Tabelle nutzt dieselben drei (server/model/network_client_reliability_partition_model.go:194), wobei network_id als Index-Payload überlebt (:63,631-634). Die Aufbewahrung beträgt 30 Tage (server/model/network_client_reliability_partition_model.go:52-69; Tagespartitionen werden als Ganzes verworfen).
  • network_client.auth_time ist ein dauerhaftes Zuletzt-gesehen, 30 Tage aufbewahrt (server/model/network_client_model.go:2556,2576); Zeilen getrennter Verbindungen werden nach 8 Stunden abgeräumt (server/taskworker/work/network_client_work.go:85).
  • Kennungen auf Kontoebene, allein beim Operator: E-Mail-Adresse oder Telefonnummer im Klartext in network_user.user_auth, das vollständige JWT des Drittanbieter-IdP bei SSO-Anmeldungen, Kennungen im Klartext für jeden Anmeldeversuch in user_auth_attempt, Wallet-Adressen, Zahlungszeilen, eine dauerhafte Wer-hat-wen-eingeladen-Kante network_referral und audit_provider_event-Zeilen, die wörtliche Land-, Regions- und Stadt-Strings gegen network_id und device_id tragen, nicht an das 8-Stunden-Abräumen der Verbindungen gebunden (review/verified/PRIVACY-ENFORCEMENT.md §1.4).
  • User-Agent wird von der Allowlist aus fünf Headern geloggt (server/http_log.go:13-29). Er ist der eine Eintrag der fünf, der eine Fingerprinting-Fläche ist.

9.4 Die WireGuard-Brücke: Die Tunnel-Adresse wird vor dem Ausgang per NAT umgeschrieben

Jedem WireGuard-Client wird eine stabile private Tunnel-Adresse aus einem durchmischten Pool von 10.000.000 RFC1918-Adressen zugeteilt (server/model/network_client_proxy_model.go:774), in die Konfiguration als Adresse der Schnittstelle geschrieben (:756-771). Diese Adresse ist dauerhaft: Sie ist Teil der Konfiguration des Peers und überdauert jede Sitzung.

Sie erreicht die Provider nicht. Das gehostete Gerät ersetzt sie beim Ausgang durch eine Adresse pro Gerät und stellt bei der Rückkehr die eigene des Peers wieder her. Der Ersatz wird einmal bei der Konstruktion des Geräts aus demselben 169.254.0.0/16-Pool genommen, den das tun des App-Pfades nutzt (server/proxy/proxy_device.go:954, über connect.TakeLocalIpv4Address, connect/tun.go:471), und beim Schließen zurückgegeben; er gilt also pro Gerät statt pro Konto, und er stammt aus demselben Raum wie jede andere lokale Tunnel-Adresse, statt unverwechselbar zu sein. Die Umschreibung besorgen natRewriteEgress (server/proxy/proxy_device.go:1297) und natRewriteReturn (:1314), über connect.RewriteIpv4Source/RewriteIpv4Destination (connect/ip_nat_rewrite.go:71,77), die die IPv4-Header-Prüfsumme und jede Transport-Prüfsumme über den Pseudo-Header inkrementell reparieren. Rückpakete werden anhand der ersetzten Adresse zugeordnet, denn an diese hat der Provider sie adressiert (receiveWithNotifyNat, server/proxy/proxy_device.go:1339).

Drei Grenzen, die es wert sind, genannt zu werden, denn das entfernt eine Verknüpfung und nicht die anderen. Nur der eigene Datenverkehr des angeschlossenen Peers wird umgeschrieben, ein Gerät, das zusätzlich HTTP oder SOCKS bedient, lässt diese Flows also unberührt. Ist der lokale Pool erschöpft, schaltet sich die Umschreibung selbst ab, statt Datenverkehr zu verwerfen — das alte Verhalten ist also weiterhin der Fehlermodus, eine Entscheidung für Verfügbarkeit und der Grund, warum die Eigenschaft „hält normalerweise“ lautet und nicht „kann nicht versagen“. Und jeder Provider in einem Fenster sieht für die Lebensdauer dieses Geräts weiterhin dieselbe ersetzte Adresse, genau so, wie die tun-Adresse des App-Pfades über die Provider einer Sitzung hinweg geteilt wird (§9.1); entfernt wird die sitzungsübergreifende, an das Konto gebundene dauerhafte Handhabe, nicht die Korrelation innerhalb einer Sitzung.

Das Verhalten ist festgenagelt durch TestWgNatReplacesThePeerAddressOnEgress, TestWgNatRoundTripIsExact, TestWgNatLeavesOtherSourcesAlone und TestWgNatReturnIsMatchedOnTheNatAddress (server/proxy/proxy_device_nat_test.go), die Prüfsummen-Arithmetik durch zwölf Tests in connect/ip_nat_rewrite_test.go, die gegen eine vollständige Neuberechnung prüfen, statt die inkrementelle Rechnung nachzubilden.

Zusammen mit dem festen Resolver 1.1.1.1 (§3.2) und dem Fehlen einer versiegelten Sitzung auf diesem Pfad (§2.4) bleibt die WireGuard-Brücke der am wenigsten private Weg, das Netzwerk zu nutzen. Die Apps und das SDK sind der privateste.

10. Rechtliche Anordnungen

Gerichtsstand. Vertragspartner ist BringYour, Inc., eine C-Corporation aus Delaware (docs/legal/terms.md:8-9 nennt die Gesellschaft; die Inkorporation wird dort nicht genannt), mit einer Anschrift für Mitteilungen in der 2261 Market Street #5245, San Francisco, CA 94114 (docs/legal/terms.md:265-271). Anwendbares Recht ist das von Texas, Gerichtsstand ist Harris County, Texas (docs/legal/terms.md:314-321) — eine bewusste Wahl und kein Widerspruch zur kalifornischen Adresse: Die Rechtsberatung des Unternehmens sitzt in Texas. Die eine Lücke, auf die ein Leser stößt: Die veröffentlichten Bedingungen nennen weder den Gründungsstaat noch einen Registered Agent, die für eine Zustellung gerichtlicher Schriftstücke nötigen Unternehmensangaben lassen sich aus ihnen allein also nicht ermitteln. Alles davon liegt in den USA, und alles davon ist durch US-Zwangsverfahren erreichbar, einschließlich Verfahren, die mit einer Schweigeverfügung kommen. Die Bedingungen erklären, dass der Operator „Informationen überwachen und offenlegen darf, wo Gesetz oder behördliche Anordnung dies verlangen“ (docs/legal/terms.md:204-206).

Was eine Anordnung an den Operator erreicht. In absteigender Reihenfolge der deanonymisierenden Kraft: wer das Konto ist — E-Mail-Adresse oder Telefonnummer im Klartext, das JWT des Drittanbieter-IdP bei SSO-Anmeldungen, Wallet-Adressen und Zahlungsdatensätze, die über Stripe, Apple, Google Play oder eine tx_signature on-chain an eine echte Identität binden; wann es sich verbunden hat und aus welcher Stadt, pro Verbindung; welche Provider es genutzt hat und für wie viele Bytes; und einen Hash mit geheimem Schlüssel über den /29- oder /56-Block, aus dem es sich verbunden hat, dazu den Quellport im Klartext. Dritte halten mehr: Die in §1 genannten Zahlungsdienstleister halten Identität, die der Operator nie speichert, und eine Solana-tx_signature löst sich zu einer Wallet auf einer öffentlichen Chain auf.

Was eine Anordnung nicht erreicht, weil es nicht existiert. Das Transferregister hat nirgends ein Feld für Ziel, Host, URL, SNI, Port oder Domain (review/verified/PRIVACY-ENFORCEMENT.md §1.2). Hochgeladene Support-Logs werden beim Empfang verworfen. Es gibt keinen Browserverlauf herauszugeben, auf keinem Pfad, versiegelt oder nicht — das ist die stärkste strukturelle Eigenschaft in diesem Dokument, und sie gilt unabhängig davon, ob sich jemand anständig verhält.

Was eine Anordnung mit Blick nach vorn erreicht. Alles bisher Gesagte betrifft gespeicherte Datensätze. Eine Anordnung, die künftiges Verhalten erzwingt, ist etwas anderes, und §7.4 ist die Stelle, an der sie greift: Ein Operator, der gezwungen wird, einen bestimmten Nutzer abzuhören, könnte heute Provider-Schlüsselmaterial austauschen oder weglassen und den Datenverkehr dieses Nutzers lesen, selbst bei eingeschalteter Post-Quanten-Verschlüsselung, wobei das einzige Signal eine Zeile im Gerätelog wäre. Die versiegelte Sitzung erhöht die Kosten rückwirkender Offenlegung. Sie widersteht in ihrer heutigen Form keinem gezwungenen Operator mit Blick nach vorn.

Die Kontolöschung ist unvollständig. RemoveNetwork ist eine von Hand geschriebene Löschliste, keine Datenbank-Kaskade (server/model/account_model.go:108). Sie entfernt die network_user-Zeilen und ihre Auth-Datensätze (Passwort, SSO, Wallet, Seedphrase), die network-Zeile und den Eintrag im Namensindex, und sie plant eine Stripe-Kündigung ein. Sie löscht nicht account_payment, stripe_customer, apple_subscription_transaction, abgeschlossene solana_payment_intent-Zeilen, network_referral, Gerätezeilen oder Audit-Zeilen; die altern nach ihren eigenen Zeitplänen aus — Audit-Ereignisse nach 180 Tagen (server/model/audit_model.go:1018,1052) — oder, bei abgeschlossenen On-Chain-Zahlungen, gar nicht (server/model/solana_payment_intent_model.go:96,137 löscht nur Intents mit einer tx_signature von null). Die Löschung schreibt außerdem eine Zeile: ein AuditEventTypeNetworkDeleted-Ereignis, geschlüsselt auf die gelöschte network_id (account_model.go:108). Das „Konto und die zugehörigen persönlichen Daten löschen“ der Datenschutzerklärung (docs/legal/privacy.md:69) ist weiter gefasst als das, was der Code tut.

Es gibt keinen Warrant Canary, keinen Transparenzbericht und kein veröffentlichtes Verfahren für Strafverfolgungsbehörden. Als fehlend verifiziert über docs/ und den Quellcode der ur.io-Website. Eine Adresse für Rechtsverfahren gibt es überhaupt nicht: [email protected] ist auf Schwachstellenmeldungen begrenzt (docs/legal/vdp.md:42), und [email protected] ist die allgemeine Adresse für vertragliche Mitteilungen (docs/legal/terms.md:265); keine der beiden ist ein Kanal für die Zustellung gerichtlicher Schriftstücke. Wer dem Unternehmen etwas zustellen will, findet in den veröffentlichten Bedingungen nichts, woran er sich halten könnte, außer der Anschrift für Mitteilungen in San Francisco. URnetworks eigene Vergleichsdokumente räumen den fehlenden Canary und den fehlenden Transparenzbericht gegenüber Mitbewerbern, die einen veröffentlichen, bereits ein, und dieses Dokument wiederholt es, statt es abzumildern.

Die Datenschutzerklärung schweigt, wo sie konkret sein müsste. Sie erklärt, „alle persönlichen Informationen, die wir von und über Nutzer erheben, beschränken sich auf E-Mail-Adresse oder Telefonnummer“, und die Erhebung geschehe „direkt von dir, wenn du sie angibst“ (docs/legal/privacy.md:29-37).

Ein früherer Entwurf dieses Abschnitts nannte das eine zu knappe Beschreibung des Systems, mit der Begründung, der Code speichere mehr Kategorien, als die Erklärung benennt. Diese Einordnung wurde am 2026-08-09 als falsch zurückgezogen. Alles andere, was dieses Dokument inventarisiert, sind entweder Kontodaten, die der Nutzer durch Registrieren oder Bezahlen selbst erzeugt (den Stripe-Verknüpfungsschlüssel gibt es, weil jemand zugestimmt hat, abgerechnet zu werden), oder gar keine persönlichen Informationen: ein Quellport und ein Block-Hash mit geheimem Schlüssel, den der Operator nicht umkehrt. Ob ein Hash mit geheimem Schlüssel im Prinzip von seinem eigenen Inhaber invertiert werden könnte, ist ein echter technischer Vorbehalt — §5 benennt ihn, und die fehlende Rotation ist eine echte Schwäche —, aber ein Wert, den niemand nachschlägt, ist keine Kategorie persönlicher Informationen, die zu deklarieren die Erklärung versäumt hätte.

Was der Erklärung tatsächlich fehlt, ist etwas anderes und Engeres: keine Aufbewahrungsfrist, keine Aussage zum Logging und kein Abschnitt zu Strafverfolgung oder behördlicher Offenlegung. Die Zeichenketten retention, log, IP address, subpoena und legal process tauchen in ihr nicht auf. Das fällt ins Gewicht, weil die Aufbewahrungsdisziplin im Code existiert und stärker ist, als die Erklärung behauptet — §5.1 legt die verifizierten Fenster dar. Ein Nutzer, der die Erklärung heute liest, kann den Operator an keinem von ihnen festhalten.

11. Wogegen URnetwork nicht schützt

Drei Dinge gelten, und es lohnt sich, sie vor der folgenden Liste zu benennen, damit die Liste als Grenze gelesen wird und nicht als Urteil. Auf einem Relay-Pfad erfährt ein Provider nie, wer du bist, und keine Einstellung und kein Provider-Build ändert das (§2). Es gibt nirgends im Transferregister ein Feld für Ziel, Host, URL, SNI, Port oder Domain, es gibt also keinen Browserverlauf, den man erzwingen, leaken oder verkaufen könnte (§5, §10). Und jede einzelne dieser Aussagen ist in öffentlichem Quellcode nachlesbar, und deshalb kann dieses Dokument bei den eigenen Fehlern konkret werden.

Alles Folgende ist ein Versagen. Ohne Absicherungen benannt; jeder Punkt ist oben ausgeführt.

  1. Ende-zu-Ende-Timing- und -Verkehrskorrelation. Kein Padding, kein Cover-Traffic, kein Mixing. Ein Angreifer, der sowohl dein Zugangsnetz als auch die Provider beobachtet, die deinen Datenverkehr tragen, kann beides korrelieren. §8.1.
  2. Ein globaler passiver Angreifer. Vollständig außerhalb des Geltungsbereichs. §8.3.
  3. Kollusion zwischen Operator und Provider. Der Operator weiß, wer du bist, der Provider weiß, wohin du gegangen bist, und der Vertragsdatensatz verbindet beides. Die Versiegelung verengt, was der Operator allein hält; sie zerbricht die Verbindung nicht. §6.2.
  4. Ein Operator, der nie ehrlich war und Provider-Schlüsselmaterial austauscht. Ein Austausch kostet jetzt ein signiertes, dauerhaftes, für Dritte erkennbares Artefakt, und ein Operator, der feindselig wird oder inkonsistent ist, wird erwischt. Einer, der ab deinem ersten Kontakt feindselig und in sich konsistent ist, gewinnt weiterhin, denn der Client hat keine vom Operator unabhängige Sicht darauf, welcher Signierer maßgeblich ist. §7.4.
  5. Gar keine Versiegelung BEI AUSGESCHALTETER Post-Quanten-Verschlüsselung, dem ausgelieferten Standard. In diesem Modus startet der Client nie eine Sitzung, sein Datenverkehr ist also von Anfang an unversiegelt, ohne für den Nutzer sichtbaren Hinweis. Behoben für den Modus, der danach verlangt, am 2026-08-10: Ist der Schalter an, läuft der Client fail-closed und weigert sich, Anwendungsdaten im Klartext zu senden oder anzunehmen. Was in beiden Modi bestehen bleibt, ist die fehlende Anzeige — keine App zeigt, ob eine gegebene Verbindung versiegelt ist. §2.2.
  6. Ein feindseliger Provider, der unverschlüsselten Datenverkehr manipuliert. HTTP im Klartext wird unverändert durchgereicht; der Provider steht in der Position eines feindseligen Hotspots. §3.
  7. Kompromittierung des Endpunkts. Schadsoftware, ein kompromittiertes Betriebssystem, eine feindselige Browser-Erweiterung oder jeder, der dein entsperrtes Gerät in der Hand hat. Nichts an einem Netzwerkprodukt geht das an.
  8. Identifizierung auf der Zielseite. Cookies, Anmeldungen, Browser-Fingerprinting und Kontoverhalten identifizieren dich gegenüber den Websites, die du besuchst, unabhängig davon, wie die Pakete angekommen sind. Deine Exit-Adresse zu wechseln macht dich gegenüber einem Dienst, bei dem du dich anmeldest, nicht anonym.
  9. Identität über den Zahlungsweg. Karten- und App-Store-Wege legen deine Identität beim Zahlungsdienstleister ab, auch wenn der Operator sie nicht speichert. On-Chain-USDC hinterlässt eine öffentliche tx_signature.
  10. Sybil-Provider. Bereitstellen kann jeder, ohne Attestierung und ohne Stake. Eine einzelne Partei kann viele Provider betreiben, auch der Operator.
  11. Das Fehlen einer Versiegelung auf dem Browser- und dem Proxy-Pfad. Die Browser-Erweiterung hat keine Einstellung für Post-Quanten-Verschlüsselung, und auf diesen Pfaden betreibt der Operator den Client. §2.4.
  12. Korrelation innerhalb einer Sitzung auf der WireGuard-Brücke. Die dauerhafte Tunnel-Adresse des Peers wird vor dem Ausgang per NAT umgeschrieben, sie folgt einem Konto also nicht mehr über Sitzungen hinweg — aber jeder Provider in einem Fenster sieht weiterhin dieselbe ersetzte Adresse, genau so, wie die Provider auf dem App-Pfad eine einzige tun-Adresse sehen. Und ist der lokale Adresspool erschöpft, schaltet sich die Umschreibung selbst ab, statt Datenverkehr zu verwerfen, das Verhalten vor dem NAT bleibt also der Fehlermodus. §9.4.
  13. Eine dauerhafte Handhabe zur Identität für jeden, der auch bereitstellt. Der Ed25519-Schlüssel des Provider-Clients überlebt gewollt die Rotation von Client-ID und Geräte-ID, und GET /key/ ist nicht authentifiziert, jede Partei kann also Client-IDs zusammenfassen, die sich einen Schlüssel teilen. §9.2.
  14. Datenverkehr, den das lokale Netzwerk im DNS-Fenster beim Start sieht. Im Quellcode als hingenommener Preis dokumentiert. §3.2.
  15. Alles, was eine externe Prüfung gefunden hätte. Für das Protokoll und den Server-Code wurde keine durchgeführt.

12. Was dieses Dokument nicht klären konnte

Eine ungeklärte Behauptung in einem Bedrohungsmodell ist eine Behauptung, die auch nicht in der Dokumentation stehen sollte. Diese sind offen.

  • Aufbewahrung und Weiterleitung durch journald auf den Hosts. Dienst-Logs werden jetzt am Deskriptor bereinigt, bevor sie den Prozess verlassen (§5), was journald erreicht, sollte also außerhalb eines Crash-Dumps keine Adressen tragen. Was diese Quellcode-Durchsicht nicht klären kann, ist, wie lange journald das Empfangene aufbewahrt, wohin es dieses weiterleitet und ob ein Crash-Dump — der konstruktionsgemäß unbereinigt durchgeht — länger aufbewahrt wird als der Rest. Das ist eine Frage des Deployments, keine des Codes.
  • Ob die Namensauflösung auf dem Browser-Pfad jemals lokal geschehen kann. Die Erweiterung konfiguriert standardmäßig einen HTTPS-CONNECT-Proxy, der aus der Ferne auflöst, und die SOCKS-Namensauflösung läuft über den DoH-Dialer des Tunnels (connect/tun.go:517-523). Das Proxy-DNS-Verhalten je Browser unter jeder Konfiguration wurde nicht getestet.
  • Die tatsächliche Verbosität in Produktion. BY_LOG_V steht standardmäßig auf 0 (server/env.go:46), was das an V(1) gebundene Ziel-Logging auf Client und Server unterdrückt — aber das einzige Deployment-Manifest im Baum setzt es auf 2 (xops/gitops-unused/.../api/deployment.yaml:47-48), in einem Verzeichnis namens gitops-unused. Behandle Verbosität als Laufzeit-Flag, nie als strukturelle Garantie.
  • Unabhängigkeit der Flotte. Der Operator veröffentlicht Live-Zahlen zu Städten und Ländern aus der eigenen Aggregation über aktuell verbundene, gültige Provider (server/model/network_client_location_model.go:3525), und der Standort wird vom Operator aus der Verbindung abgeleitet, die er beobachtet, statt vom Provider deklariert zu werden (review/verified/ARCHITECTURE.md §6.4). Aber keine unabhängige Partei hat die Flotte von außen vermessen, und nichts weist nach, dass die einem bestimmten Client angebotenen Provider voneinander oder vom Operator unabhängig sind. Der Mechanismus ist im Quellcode überprüfbar; die Population ist heute für eine dritte Partei nicht überprüfbar.
  • Die Bindung des Identitätsschlüssels bei der Kontoerstellung. Das Design formuliert die Erwartung, dass „die Bindung (ClientId, öffentlicher Schlüssel) bei der Kontoerstellung registriert wird“ (connect/transfer_key.go:60-85). Ausgeliefert wird ein Client, der seinen Schlüssel in ein vom Operator kontrolliertes Redis veröffentlicht, wieder ausgeliefert von einer vom Operator kontrollierten API. Ob betrieblich eine stärkere Registrierung existiert, ließ sich aus dem Quellcode nicht klären; geh davon aus, dass es sie nicht gibt.
  • Ob Roles und Principal bei gewöhnlichen Verbraucher-Clients jemals gefüllt werden. Es sind vom Operator zugewiesene Strings, die in die signierten Vertragsbytes eingeschlossen sind und nur für ProvideMode_Network gesetzt werden (connect/protocol/transfer.proto:547-561; server/controller/connect_controller.go:831-838). Es fand sich kein Beleg dafür, dass die Apps sie füllen, und nicht jeder Aufrufer wurde auditiert. Würden sie jemals für Verbraucher-Datenverkehr gefüllt, wären sie vollwertige sitzungsübergreifende Kennungen, die für einen Provider sichtbar sind.
  • Persistenz des Schlüsselmaterials auf nicht-mobilen Hosts. ClientKeySeed wird nur aus sdk/local_state.go, sdk/device_local_key_material.go, sdk/device_local.go und sdk/cgo/ referenziert. Hosts, die diese nicht aufrufen, bekommen pro Prozess einen frischen Schlüssel; das Verhalten von Linux, Windows, Erweiterung und Server-Proxy wurde nicht einzeln verifiziert.

13. Meldungen

Schwachstellen: [email protected] und die Offenlegungsrichtlinie unter ur.io/vdp. Korrekturen an diesem Dokument, einschließlich Widerspruch zu jedem Urteil darin, sind auf demselben Weg willkommen — ein unabhängiger Befund, der einer Behauptung hier widerspricht, ist nützlicher als die Behauptung.