Threat model
This document names URnetwork's adversaries and says what the system does and does not do about each one. It is written to be attacked. Where a property is conditional, the condition is in the same sentence; where something is a design intention that the shipping code does not yet enforce, it is labelled design intent, not a property. A summary for readers who will not read the full record precedes section 1.
The benchmark is Tor's own explanation of the attacks onion routing does not defeat: an adversary who observes both ends of a circuit can correlate them by timing, and Tor says so in public. URnetwork ships no cover traffic and no padding, so the same is true here, and section 8 says it without softening.
Assurance status, stated once and applying to everything below. There is no independent audit of the protocol, the connect engine, or the operator's server code — the layer the privacy claims rest on. Two 2025 third-party assessments cover other surfaces: an independent third-party penetration test of the web application and API (25 April – 5 May 2025, credentialed, manual, OWASP Top 10 plus an ASVS controls review), and a Leviathan Security Group MASA AL2 assessment of the Android app (completed 23 May 2025), which passed and which Leviathan itself scopes as "not a holistic security evaluation or comprehensive penetration test". Neither examined logging, retention, the data path, or the protocol. Every claim in this document is therefore a claim about readable source, which anyone can check and nobody outside the project has yet checked as a whole.
How to read the citations
Claims cite path:line against the working trees at /Users/brien/urnetwork/{connect,sdk,server,proxy,extension} as of 2026-09-17, when every line-anchored citation in this document was re-resolved against the trees by identifier. Line numbers drift; the identifiers do not. Where a finding was established in an earlier code-verification pass, it cites review/verified/ARCHITECTURE.md or review/verified/PRIVACY-ENFORCEMENT.md, which carry the same convention.
That re-resolution was not cosmetic and is worth recording as a caution for anyone reading an older copy. Between the 2026-08-07 pass and this one connect/transfer.go roughly tripled in length, and almost every connect citation moved — the fail-closed send gate cited at transfer.go:2796 is at :6751 today, and the receive gate cited at :5844 is at :14886. Treat any line number in a copy of this document that does not carry the date above as unreliable, and navigate by identifier.
Three labels are used deliberately:
- Property — the code enforces it, and an attacker who controls the other parties still cannot violate it.
- Discipline — the code chooses not to do something it is capable of doing. A deployment change or a one-line patch could reverse it.
- Design intent — the design says this is the goal; the shipping code does not yet enforce it.
Summary
This section is for readers who will not read the full record. Each sentence in it is backed by a numbered section below or by the assurance statement above.
What the system is. URnetwork is a privacy network in which members relay traffic for other members. The path is you → extender → operator → provider → internet. The trust split is between two relay parties: the operator, which knows who you are, and the provider, which sees where your traffic goes.
The question this document exists to answer. Can an untrusted operator and an untrusted set of providers still carry a private and anonymous session, so long as they are not colluding? The honest answer today is most of the way, and the gap is precisely locatable: provider blindness to identity holds against an arbitrarily malicious provider, unconditionally; operator blindness to content holds against a passive operator and against any on-path attacker, but rests on the operator distributing provider identity keys honestly and on its not choosing your providers adversarially. §1.1 is the trust model that states this party by party; §7 is the mechanism behind the caveat.
The two properties, separately scoped. On a relayed path the provider never receives your real source IP; no setting or provider build changes that (§2). The operator cannot read the sealed client-to-provider session, which is available on the five native apps (Android, iOS, macOS, Windows and Linux) under the name Post Quantum Encryption (§2.1). The browser extension has no sealed session — its client device runs inside the operator, which acts as a protocol translation point, so a seal cannot exist there (§2.4).
Three trust dependencies, and where each stands. The seal used to fail open silently in every mode. Fixed 2026-08-08/10: with Post Quantum Encryption on, the client runs fail-closed and refuses to send or accept plaintext application data rather than degrading to it (§2.2). Two things remain, and both are trust in the operator rather than in the cryptography:
- Key distribution. Can the operator substitute a provider key? Substantially harder since 2026-09-17, and no longer deniable. With Post Quantum Encryption on, the client withholds the session cipher until the contract-supplied provider key is corroborated against a signed, hash-chained, domain-bound registration history the operator has published, and a verified disagreement kills the session for that provider (§7.4). An operator that substitutes must now sign the substitution, leaving a permanent attributable record that diverges from what every other reader of that client id sees. What it does not stop is an operator that is malicious from your first contact and consistent about it: it signs one coherent chain naming its own key, and a client with no independent view of which signer is authoritative cannot tell. Signed is not unforgeable-by-the-signer.
- Provider selection. The operator ranks and returns the provider set, and the client has no way to check that the providers it was offered are independent of each other or of the operator (§5, §6.1). An untrusted operator therefore need not collude with providers; it can select its own.
- And the default. The fail-closed guarantee is bound to a toggle that still ships off in all five native apps (§2.2), so a default-configuration client opens no session to the provider at all: every application packet crosses the operator unsealed, on the standard path.
verify/BEFORELAUNCH.md items 7, 8 and 9 track all three; §2.2, §6 and §7.4 describe the current state.
What the system does not defend against. An adversary who observes both your access network and the providers carrying your traffic can correlate the two ends by size and timing (§8.1). A global passive observer is out of scope entirely (§8.3). URnetwork ships no padding, no cover traffic and no mixing; section 11 is the full list.
Assurance. There is no independent audit of the protocol, the connect engine, or the operator's server code; two 2025 third-party assessments cover other surfaces: a penetration test of the web application and API, and a Leviathan Security Group MASA AL2 assessment of the Android app, which passed.
Where the detail lives. Section 1 defines the parties and states the trust model party by party (§1.1). Section 2 covers the three connection modes and what each party sees in each. Sections 3 through 5 take each adversary in turn: a malicious provider, a network observer, and the operator. Sections 6 and 7 cover operator-run providers, collusion, and provider key substitution. Sections 8 through 12 cover traffic correlation, device identifiers, legal demands, the list of what the system does not defend against, and what this document could not establish. Section 13 says where to report vulnerabilities and corrections.
1. The parties
| Party | Run by | Position |
|---|---|---|
| Client | The user | The app or SDK instance; on the browser and proxy paths it runs on the operator's servers instead (§2.4) |
| Ingress LB | The operator | nginx; stamps the observed client address into X-UR-Forwarded-For (xops/gitops-unused/gitops-prod/urnetwork/connect/ingress.yaml:9 — the only ingress manifest in the tree, and its directory is named gitops-unused, so it may not describe production; §12) |
| Connect service | The operator | One or two processes on different hosts, joined by an internal exchange connection (server/connect/resident.go:395). Not "a relay server" |
| API / control plane | The operator | Auth, provider discovery and ranking, contracts, the public-key lookup (server/api/api.go) |
| Provider | Any member | Receives relayed packets and dials the destination from its own connection (connect/ip.go:7167 NewRemoteUserNatProvider; Receive at :8726, ReceiveBatch at :8486 → LocalUserNat, :731,784) |
| Extender | Any volunteer | The path's first leg: a TLS forwarding relay on an independent address. Carries the client↔platform session without terminating it, so it sees the connecting IP and cannot decrypt — the inner stream is "the client's own tls to the destination, which the extender never sees inside" (connect/extender/extender.go:35-44; connect/net_extender.go:34-48) |
| DoH resolvers | Cloudflare, Google, Quad9, OpenDNS | The app path resolves DNS over HTTPS through the tunnel to one of these four (connect/net_http_doh.go:148-166) |
| Payment processors | Stripe, Apple, Google, Solana/Circle | Hold real identity for paid accounts; the operator stores the join keys (server/db_migrations.go:1804, :4732, :2441) |
The operator is BringYour, Inc., a Delaware C corporation with a San Francisco notice address (docs/legal/terms.md:8-9,265-271 names the entity and the address; the incorporation is not stated in the published terms). Section 10 covers what that means for legal process.
A note on vocabulary, because the network's economic layer uses a different word for the same party. Providers are called miners where they are paid for the bandwidth they carry, and the network is designed to have many independently operated operators rather than one. Neither changes the cryptography: the pattern is always one client ↔ one operator ↔ one provider, and every property below is a property of that triangle. What a plurality of operators and providers changes is the plausibility of the non-collusion assumption in §1.1, not the mechanism that assumption is protecting.
1.1 The trust model
Read this table as: if this party is arbitrarily malicious and the others are honest, does the property survive?
| Property | vs. a malicious provider | vs. a malicious extender | vs. a malicious operator |
|---|---|---|---|
| Your identity is hidden from the exit (the provider never learns your address or account) | Yes — unconditional. The address is never on the wire; the provider's entry point takes ids only (§2, §3) | Yes. An extender sees your address, which your ISP already knows, and learns nothing about the exit. The exception is the provider role, which you turn on by sharing your connection: it identifies itself by its client id to the extenders it measures (§4) | No. The operator knows who you are by construction; that is its role (§5) |
| Your destinations are hidden from the coordinator (the operator cannot read the session) | n/a — the provider is the party that sees destinations | Yes. The extender forwards a session it cannot terminate (§4) | Conditional. Holds against a passive operator, against downgrade, against an operator that turns malicious later, and against one inconsistent across its own channels. Does not hold against an operator malicious and self-consistent from first contact (§7.4) |
| No plaintext downgrade (traffic is sealed or does not flow) | Yes, with the toggle on. The receive gate refuses stripped plaintext (§2.2) | Yes, with the toggle on | Yes, with the toggle on — omission becomes denial, not disclosure. No, with it off, which is still the shipping default (§2.2) |
| No browsing record exists to be compelled or leaked | Yes | Yes | Yes — structural. No destination, host, URL, SNI, port or domain field exists anywhere in the transfer ledger (§5, §10) |
| Your traffic is not correlatable end to end | No | No | No. No padding, no cover traffic, no mixing — out of scope by design (§8) |
Three things follow, and they are the honest shape of the answer to the question in the summary.
The provider is trusted with nothing, and that is enforced structurally. Not by policy, not by the provider running the official build — by the address never being sent. This is the strongest property in the document.
The operator is trusted with two things, and both are open. It distributes the provider identity keys the seal is checked against (§7), and it chooses which providers you are offered (§5, §6.1). Neither is verifiable by the client today.
Those two are not independent, and the second is the sharper one. A document that says "safe unless the operator and providers collude" is understating the problem while the operator picks the providers: it does not need to suborn anyone if it can select whoever it already controls. The design distinction is substitution versus selection (connect/DESIGNNOTES2.md §3.4). Closing substitution has a cryptographic answer and one is being built (§7.4). Closing selection needs something the client can check about the independence of a provider set, and nothing in the shipping code attempts it.
2. The three modes, and what each party sees
This table is canonical and matches OVERVIEW.md. Everything else in this document is the mechanism behind it.
| Mode | Operator sees | Provider sees | Default and availability |
|---|---|---|---|
| Relayed sealed | account/source connection, provider association, ciphertext and timing/volume | destination traffic, device/contract id, not your real source IP | native apps only, and opt-in today: ruled to be the default, still shipping off; see §2.2 |
| Relayed standard | account/source connection, provider association, inner destinations and packet bytes | destination traffic, device/contract id, not your real source IP | the native default today, since the seal ships off (§2.2), and the browser and proxy paths (§2.4); with the seal on, a provider that cannot be sealed to is skipped rather than served here (§2.2) |
| Direct | less relay involvement | your real source IP and destination traffic | opt-in, by disabling Strong Anonymization (§2.3) |
Two asymmetric statements follow, and they are not equally strong:
Provider blindness to identity is a property, and it is unconditional on both relayed paths. The provider's inbound entry point takes ids, never an address: RemoteUserNatProvider.Receive is parameterised by a TransferPath of three 16-byte ids (connect/ip.go:8726, batch form at :8486; connect/connect.go:50). No transport protobuf carries a client address, city or country — a grep over connect/protocol/*.proto for location|city|country|region returns nothing (review/verified/ARCHITECTURE.md §1.3). There is no setting that turns this off on a relayed path, and no provider build can obtain the address it is never sent.
Operator blindness to content is a setting, not yet a default — over a mechanism with one defect left. The mechanism has a name: Post Quantum Encryption, a control in the connect drawer of the Android, iOS, macOS, Windows and Linux apps, which seals an end-to-end session between your client and the provider (§2.1). The first condition is that the user finds the switch, because it still ships off (below). As of 2026-08-10 it is no longer whether the seal quietly failed to establish: with the toggle on, a connection that cannot be sealed carries no application traffic at all rather than carrying it in the clear (§2.2). What remains conditional is whether the operator, which serves the provider keys the seal is checked against, is serving them honestly (§7.4). Read that before relying on this property; it is not visible to the user.
Default at launch: ruled ON; still shipping OFF as of 2026-09-24. The product owner ruled on 2026-08-07 that the sealed session ships enabled, so that operator blindness would be the default rather than a choice the user has to make. That has not landed. The performance-profile flag is still false in all five apps — android/.../PerformanceProfileSettings.kt:34,88, apple/app/network/Shared/ViewModels/DeviceManager.swift:395,479, windows/app/src/App/SdkHost.h:192 and linux/app/src/ConnectDrawer.cpp:746 — and the SDK starts a device with no profile at all. Without the flag the window clients keep DefaultEncryptionSettings, whose mode is EncryptionModeOff (connect/transfer_encrypt.go:768-770; sdk/device_local.go:4483), so a default-configuration client today runs neither Required nor Opportunistic: it opens no session to the provider, and every application packet crosses the operator unsealed (verify/BEFORELAUNCH.md item 7, verdict: fails). Every statement in this document about fail-closed behavior is scoped to the toggle being on, and a reader should assume it is off unless they turned it on.
That gap matters more in one direction than it used to. Because the fail-closed gate is bound to this same toggle, turning the default on would propagate a real guarantee to every user rather than merely widening the reach of a silent failure — the reverse of what this section warned when the seal could fail open in every mode. What the default still cannot fix is §7.4, which is a property of the seal itself: a default-on seal an operator can machine-in-the-middle protects against a passive operator only.
The browser extension has no sealed session at all (extension/src/utils/auth-params.ts:33, performance_profile: null). That is scope, not a default, and the launch ruling does not change it.
Wherever the seal is absent — the browser and proxy endpoints (§2.4), and the native apps with the seal turned off (§2.2) — the operator terminates the client's TLS/QUIC and relays plaintext IP packets (connect/protocol/ip.proto:9-16). It chooses to parse only the routing header — FilteredTransferFrame contains nothing but the transfer path (connect/protocol/transfer.proto:85-87), and the relay unmarshals exactly that message and no more (server/connect/resident.go:3824) — which is discipline, not a cryptographic barrier.
2.1 What the seal is, precisely
A TLS 1.3 session directly between client and provider, carried as ordinary control frames through the same operator: EncryptedControl messages hold the handshake bytes and ride the normal reliable in-order Pack/Ack flow, one session per peer-pair, with the lexicographically lower ClientId taking the TLS-client role (connect/transfer_encrypt.go:30-60). Key exchange is the hybrid X25519MLKEM768, with conventional X25519 as fallback (connect/transfer_encrypt.go:376-384); the AEAD is AES-256-GCM over a 32-byte key exported under the RFC 5705 label urnetwork-connect-aead, with a 12-byte random nonce per message (:88-99 for the labels and lengths, :283-301 for the construction); identities are Ed25519, generated in-process by ClientKeyManager (connect/transfer_key.go:87-143, the only ed25519.GenerateKey call in the three trees, at :113) — so "post-quantum" scopes to the key exchange only. Mutual TLS is required (ClientAuth: tls.RequireAnyClientCert, connect/transfer_encrypt.go:406) and the peer certificate is checked at the sequence layer against the contract's commitment rather than by the TLS stack; the comment at connect/transfer_encrypt.go:386-398 explains that without mTLS the TLS-server-role side would have no PeerCertificates and contract verification would fail for half of all peer pairs.
2.2 Fail-closed when you ask for it, fail-open when you don't
Updated 2026-08-10. This section previously said the seal fails open silently with no fail-closed option. That is no longer true, and the change is the most significant hardening in this document's history — it closed the downgrade vector in both directions. What follows is the current behavior, read from source.
Encryption is still a binary property of Cipher() != nil, and a nil cipher still means the session is not usable: a failed handshake, a failed identity proof, or a contract that never carried the peer's public key all leave it nil. The structural reason is worth stating once, because everything else depends on it — the AEAD exists before the peer is authenticated and is deliberately withheld. completeHandshake parks it on derivedTlsCipher rather than exposing it (connect/transfer_encrypt.go:1719-1726), and only maybeVerifyPendingPeerIdentityProof promotes an epoch to established, and only on a verifying proof (:1928-2050, promotion at :1979-1981). Cipher() reads establishedEpoch (:2625), so to every caller an unproved peer is indistinguishable from an incomplete handshake.
One correction to earlier versions of this section: identity-proof failure is no longer merely "left unauthenticated". It now also sets identityFailedTerminal, cancels the epoch, and emits an EncryptionEventIdentityFailed (connect/transfer_encrypt.go:2005-2030). The log line still reads "session left unauthenticated" (:2027) and the wording understates what the code does.
What changed is what happens next. There are now three modes (connect/transfer_encrypt.go, EncryptionMode):
| Mode | Behavior |
|---|---|
EncryptionModeOff | The zero value. Session layer inert, everything plaintext. |
EncryptionModeOpportunistic | Seals once a session establishes, plaintext until then — or permanently if it never does. The historical behavior. |
EncryptionModeRequired | Never exposes application data in plaintext to or from a peer for which a session is expected. |
Under EncryptionModeRequired the guarantee is enforced at four points:
- Send entry gate (
connect/transfer.go:6751-6800, inSendSequence.Pack). An application pack waits for the cipher within the caller's timeout and is then refused, unsent — never downgraded — with a typedErrEncryptionRequiredNotEstablished(connect/transfer_encrypt.go:505). The gate sits deliberately before a sequence number is assigned: the client-role handshake rides this same sequence, so holding an already-sequenced frame would gap the ordered receive side and strand the ClientHello behind the gap, deadlocking the handshake that would clear the gate. - Send backstop (
connect/transfer.go:11726-11738, inwriteMaybeWrappedBytes). A frame that reaches the writer without a cipher is refused rather than written, covering the narrow race where a session is torn down between enqueue and write. - Receive gate (
connect/transfer.go:14886-14913). A plaintext application frame from a peer for which a session is expected is dropped and audited — this is the half that matters most, because it closes the downgrade where an attacker strips the wrap and the receiver would otherwise accept the plaintext. The frame is acked and discarded rather than left unacked, since withholding the ack would gap the ordered sequence and wedge both sides. - Candidate prefilter (
connect/ip_remote_multi_client.go:11695-11710,EncryptionCapabilityPrefilter, default true at:196). A window candidate that the platform's out-of-band key API says has never published an identity key is failed immediately, since it can never complete the handshake. The rejection rule is deliberately narrow (:13229-13236): a fetch error does not reject, so operator unreachability cannot be turned into a provider ban. It only accelerates certain failure; it never admits a candidate.
Handshake control frames, acks, and control-plane peers are exempt by design — the gate covers application payload, not the scaffolding that bootstraps it.
Which mode you get is the Post Quantum Encryption toggle. When it is on, the consumer client runs EncryptionModeRequired (connect/ip_remote_multi_client.go:13080-13091, from the profile flag at :1311): a provider that cannot establish a session carries no application traffic for you at all, rather than carrying it in the clear. The stated cost is availability, accepted deliberately. Providers run EncryptionModeOpportunistic (sdk/device_local_provider.go:186) so that one provider can serve both sealed and unsealed consumers; that is a responder-side compatibility choice and does not weaken the initiator's guarantee.
Note that the mode enum's zero value is Off (connect/transfer_encrypt.go:469-504), so a zero settings struct encrypts nothing rather than silently half-encrypting. That is a deliberate choice made when Encrypt bool was replaced by the tri-state.
What this means for the toggle. With PQE on, the seal no longer fails open: it fails closed, loudly, on both send and receive. With PQE off, which is how the apps ship, the client is not even opportunistic: it runs EncryptionModeOff and never starts a session, so all of its application traffic flows unsealed, and nothing tells the user. So the honest statement is conditional, and the default matters: see the Default at launch note in §2.
The behavior is covered by tests, not just comments — TestRequiredEncryptionFailsClosedAgainstPlaintextPeer, TestRequiredGateNonBlockingSendRefusesPreCipher, TestRequiredGateBoundedBudgetRefusesUnsent, TestRequiredSendRefusalTypedErrorAndEvent and TestRequiredContractFreeWithoutKeySourceFailsClosed.
One stale comment to ignore while auditing: the comment inside completeHandshake at connect/transfer_encrypt.go:1723-1725 still asserts unqualified that until the identity proof verifies, "the wrap path observes cipher == nil and traffic flows in plaintext". That is true only in Off and Opportunistic mode; the gates above override it under Required. The comment predates the fix.
Partly closed since: there is now an identity surface, though not a per-connection banner. The SDK exports the set of peers with an established, identity-verified session, together with a canonical display fingerprint — sdk/post_quantum_identity.go defines PublicIdentityKeyHash as "THE canonical display hash for identity keys on every platform: the SHA256 of the key, encoded as unpadded uppercase base32 (RFC 4648)", with ProviderIdentity at :24-30 and a view controller at sdk/post_quantum_identity_view_controller.go:21, consumed by both apps (PostQuantumIdentityViewModel.kt, PostQuantumIdentityStore.swift). That is more than a hook: it is the beginning of a user-checkable, operator-independent comparison channel, since a fingerprint obtained elsewhere can be compared against the one displayed. What is still missing is a plain per-connection "this session is sealed" indicator. The remaining device-log signals are peer identity proof verified — cipher is now usable (connect/transfer_encrypt.go:2004) on success, an Errorf on failure (:2026-2029), and a NotifyRequiredSendBlocked event when the gate refuses.
2.3 Direct mode
Turning off Strong Anonymization sets AllowDirect, whose own field comment reads: // setting this to true exposes the real source IP to the provider (sdk/sdk.go:1130-1139). The flow is forced onto a P2P stream (connect/ip_remote_multi_client_probe.go:1207-1209) over a WebRTC/ICE data channel (connect/transport_p2p_webrtc.go:818-825), and ICE means both endpoints learn each other's addresses. The default is off — a nil performance profile yields false (connect/ip_remote_multi_client.go:841-850; sdk/sdk.go:1130-1139).
Two forced cases: same-network peers (your own devices) always allow direct (connect/ip_remote_multi_client.go:843-850,2656-2660), and hosted devices force it off (sdk/device_local.go:608-615). Note that direct mode removes the operator from the data path only. Contracts, provider selection and control messages still go through the platform, so the operator still learns which provider you used and how many bytes moved.
2.4 The path the table does not cover: the browser and the proxy endpoints
On the browser extension, the web app, and the HTTPS/SOCKS5/WireGuard endpoints, the operator runs the client. The extension provisions a server-side proxy device (extension/src/utils/auth-params.ts:22-36, enable_socks/enable_http) and the browser is pointed at the operator's proxy endpoint — HTTPS CONNECT by default (extension/src/bridge/background.ts:220; extension/src/utils/proxy-manager.ts:264). The SDK instance that opens contracts and holds the provider window runs in server/proxy, not on the user's machine.
This is why no sealed session can exist on these platforms, as a matter of architecture rather than of engineering effort not yet spent. The sealed session is client↔provider, and its guarantee depends on the client endpoint living on the user's own device. A browser or a plain proxy client speaks its own protocol (HTTPS CONNECT, SOCKS5, WireGuard) and cannot run the network's engine, so the operator runs the client's device remotely and acts as the protocol translation point between the two. A seal negotiated from that remote device would start inside the operator — the party the seal exists to blind. The implementation accepted this tradeoff deliberately: these surfaces exist for convenience on platforms and protocols that cannot host the full client, and the docs must not describe them as sealed.
Consequences, stated plainly:
- Provider blindness to identity still holds — the provider still receives only ids.
- Operator blindness does not exist on this path in any form. The operator terminates the proxy connection, receives the destination host from CONNECT, and holds the client's keys.
- The hosted device installs no DNS-upgrade mux (
server/proxy/proxy_device.go:858,SetUpgradeMuxSettings(nil)), so the app path's in-tunnel DoH (§3.2) does not apply here.
What stands in for encryption on this path is storage discipline: the proxy data path logs nothing on client-drivable paths, and a regression test pins it — proxy/socks5_nolog_test.go, TestClientDrivenTrafficNeverLogs, asserts zero log lines across malformed, oversize and undialable cases. Two honest caveats: the test's stated motive is log-amplification DoS rather than privacy, and the panic-recovery handler does log a client address (proxy/socks5_server.go:129-132). See review/verified/PRIVACY-ENFORCEMENT.md §2.2.
3. Adversary: a malicious provider
In scope, and assumed. Anyone can provide. There is no attestation, stake, identity check or Sybil resistance on provider participation — a search across the server, connect and sdk trees for sybil|kyc|attestation returns nothing relevant. A provider runs open-source code on hardware it controls, so assume it runs a modified build.
What it sees. Everything an ISP sees for the traffic it carries: destination IP and port, packet sizes and timing, and TLS SNI where the client sends it in the clear. It receives raw IP packets and dials them itself (connect/protocol/ip.proto:9-16; connect/ip.go LocalUserNat). It also sees the client_id carried as the contract's SourceId (connect/protocol/transfer.pb.go:1779; server/controller/connect_controller.go:816-830) — a per-window-slot identifier that only the operator can resolve to an account, and never the device_id, which has no field on the wire. Its lifetime, and the several things that do persist across sessions, are in §9.
What it can do.
- Plaintext HTTP: read and modify it. Port 80 is passed through to the egress unchanged by default —
HttpUpgradeUnencryptedis the default mode (connect/ip_mux_upgrade.go:38-47). A provider is in the same position as a hostile Wi-Fi hotspot for any traffic that is not itself encrypted. HTTPS protects application content from the provider; nothing in URnetwork adds a second layer over the destination's own TLS. - Selectively drop, delay or stall traffic. A provider that acknowledges traffic but returns none is labelled a black hole and removed (
connect/ip_remote_multi_client.go:37-54), so denial is detected and routed around — but detection is statistical, and the traffic it saw before removal was still seen. - Lie about performance to attract traffic. Ranking uses measured latency and throughput (
server/model/network_client_location_model.go:4098,4220), and measurement is the operator's, not the provider's self-report, so this is bounded — but it is a ranking input, not an integrity control. - Correlate flows within its own view. Per-site affinity pins a site to one provider (
connect/ip_remote_multi_client.go:837), so a single provider sees a coherent slice of one client's browsing for the sites it holds.
What it cannot do without help. Learn your real IP on a relayed path, learn your account or email, or attribute a client_id to a person. Those require the operator (§5).
3.1 What ip_security does and does not constrain
The connect engine's ip_security layer runs on the provider's own egress and on the user's own device, never centrally at the operator — the operator's IngressSecurityPolicyGenerator/EgressSecurityPolicyGenerator fields exist but are never assigned or read (server/connect/resident.go:263-264; review/verified/PRIVACY-ENFORCEMENT.md §4.1). It blocks BitTorrent and file-sharing signatures, opaque non-standard protocols, reputation-listed destinations and attack-pattern traffic, and it does so on a deliberately thin basis: 5-tuple plus a payload prefix bounded at 8 packets / 512 bytes.
The reputation tables are generated, not hand-kept. connect/security and connect/blocker aggregate public threat-intelligence feeds — abuse.ch Feodo Tracker botnet C2, Spamhaus DROP and DROPv6, Emerging Threats compromised IPs, Blocklist.de, CINS Army, TweetFeed, ViriBack, BruteForceBlocker — into packed range tables (46,789 IPv4 ranges as of the 2026-08-05 snapshot; connect/ip_security_cfaa_block.go header carries the feed list and attribution). The release pipeline regenerates both tables from the live feeds (build/all/run.sh, the CONNECT_IP_UPDATE step), so each release ships a current snapshot and an up-to-date client blocks the latest listed address space. The snapshot ages with the installed build; the lists update per release, not over the air.
The payload-prefix bound is connect/ip_security_dmca.go:136, with the server name cleared out of the flow key (:366,386,424) and counters keyed on (version, protocol, port) with the IP field never enabled (connect/ip_security.go:371).
It is a safety control for honest providers, not a security control against hostile ones. It constrains what a provider's connection will carry outbound. It does nothing about what a provider does with traffic it does carry, it runs in a process the provider owns, and a modified build can disable it. Do not read it as a limit on a malicious provider.
One reporting detail belongs here because it is a metadata channel. A BitTorrent signature match returns Incident, which calls ReportAbuse (connect/ip.go:9014,9110), transmitting a PeerAudit control frame to the operator carrying the peer's device id and a boolean — no destination, no domain, no contents (connect/transfer.go:2971). The operator has no handler for that message type today, so nothing is stored on receipt (server/controller/connect_controller.go:132,436-437; a repo-wide grep for PeerAudit in server returns zero hits). That is a discipline one case statement away from changing. Opaque-encrypted drops are silent with no report (connect/ip_security_dmca.go:483-486).
3.2 DNS
On the app and SDK path, plain DNS on UDP/TCP 53 is intercepted and resolved over DoH through the tunnel (connect/ip_mux_upgrade.go:120-148, installed by default at sdk/device_local.go:7105), to Cloudflare, Google, Quad9 or OpenDNS (connect/net_http_doh.go:150-155). A provider therefore sees an HTTPS connection to a public resolver, not the query. Two honest limits:
- A startup DNS-leak window is documented in the source. While the tunnel DoH is still establishing, a query is raced against a resolver over the local host egress, "at the cost of a brief DNS leak during startup" (
connect/ip_mux_upgrade.go:120-126,78-92). Your local network and ISP can see those queries. - The WireGuard endpoint hands the client a fixed public resolver,
DNS = 1.1.1.1(server/model/network_client_proxy_model.go:774), and port 53 is passed uninspected by the security policy (connect/ip_security_cfaa.go:113). On that path DNS queries traverse the provider as plain DNS, and the provider can read and answer them.
The four DoH resolvers are third parties that see queries arriving from a provider's egress address, unlinked to your account. That is a real dependency and it is not configurable to zero: DNS has to be answered by somebody.
4. Adversary: a network observer
Four positions, from weakest to strongest.
Your local network and ISP. They see that you connect to connect.bringyour.com over TLS/QUIC, or to an extender, plus sizes and timings. They do not see destinations inside the tunnel, except in the documented startup DNS window above. Where the platform is blocked, extenders present as ordinary services on believable ports and clients rotate personas with randomised fragmentation (connect/net_extender.go:123-133; built at connect/net_extender_network.go:915), and a DNS-shaped transport exists for networks where only DNS escapes (connect/transport_pt.go:37,73). These are mechanisms for crossing local and regional firewalls, ranked below the direct transports (connect/transport.go:29-33). They are not traffic-analysis defences and should never be described as such.
An extender operator. Anyone can run one, the app accepts a manually entered IP, and by default no signature is required (connect/extender/extender.go:35-44). An extender is the TCP peer, so it sees the connecting user's IP, and it cannot decrypt what it forwards, because the TLS session runs through it to the platform rather than terminating on it (connect/net_extender.go:34-48). The extender leg therefore puts a first-hop observer of your address in the path who is not the operator. That is the trade the design makes to get you through a block, and it is worth being exact about its size: the extender leg adds no encryption and no anonymity — it learns what your ISP already knows and nothing more, with the one exception below. In the shipping client the direct transports are raced first and the extender dialers expand when those fail (connect/net_http.go:1686-1709); a client configured with custom extenders uses them as its only route (connect/net_http.go:1686-1709).
The exception is the provider role. Every client probes extenders to rank them by round trip. A device attests what it measured only while it provides: attestation starts when you switch providing on and stops when you switch it off, even partway through a probe pass, and a device that never provides probes anonymously (sdk/device_local_provider.go:1155-1167; connect/net_extender_network.go:1240). While it provides, each claim carries its client id and is signed with its client key (connect/DESIGNNOTES4.md §1, §3). The extender learns that the provider with that id, and so that public key (§9.2), measured it from that address; the operator receives the provider id, the extender, the round trip and the time. Extenders measure each other the same way, a target co-signs each round trip it accepts, and the operator keeps the pings for a day to derive locations (connect/GEOMAP.md §2). This is operational data about public infrastructure, under a key that is already durable and public in the provider role.
An observer of one end. Watching only the client side yields "this user is connected to URnetwork" and a byte/timing profile. Watching only a provider's egress yields destinations and a byte/timing profile with no identity attached.
An observer of both ends. See §8. This is the adversary URnetwork does not defeat.
5. Adversary: the operator
The operator authenticates clients, selects and ranks providers, authors contracts, distributes public keys, and carries the packets on the relayed paths. Assume for this section that it is hostile or compelled.
What it sees, without any additional effort: your account; your address at the moment you connect; a city/region/country derived from that address by a local database lookup — ipDb in server/ip.go opens the bundled MaxMind GeoLite2-City database (mmdb/geolite2.mmdb) from disk and ip.go makes no network call of any kind, so an address is never handed to a geolocation service — persisted per connection (SetConnectionLocation in server/model/network_client_location_model.go); which providers served you; byte counts; and on the standard path, the destination addresses and packet bytes inside the tunnel — though those are normally already inside the destination's own HTTPS.
What it stores. Connection and auth records keep a keyed one-way hash of the surrounding /29 (IPv4) or /56 (IPv6) block rather than the address (server/ip.go:46-72). Precision that matters to an auditor: it is a single process-wide pepper from the vault, memoized once, with no per-row salt and no rotation mechanism anywhere in the repo (server/ip.go:42-43); the IPv4 keyspace is 2^29 blocks, so anyone holding the pepper can invert it by brute force. The pepper's secrecy is the whole security property. The /verify subsystem uses /48 for IPv6, not /56 (server/ip.go:74-89; server/model/verify_model.go:113-116). The source port is stored in cleartext beside the hash (server/db_migrations.go:2044). The account-creation audit row records the peppered hash and port, never the raw ip:port (server/model/network_model.go:1157), and audit rows are removed after 180 days (server/model/audit_model.go:1018,1052, scheduled at server/taskworker/taskworker.go:85-88).
What does not exist to be stored. No destination, host, URL, SNI, port or domain appears anywhere in the transfer ledger — a grep of the full migration file for those terms returns zero hits (review/verified/PRIVACY-ENFORCEMENT.md §1.2). HTTP logging passes an allowlist of exactly five headers (server/http_log.go:13-29), pinned by test. No Prometheus metric carries an IP, host, destination, port or client id; of 39 declared metrics only three are labelled and every label is a bounded enum (review/verified/PRIVACY-ENFORCEMENT.md §2.5). Support-tab log uploads are drained to io.Discard (server/controller/log_file_controller.go:76); only metadata persists.
5.1 How long any of it is kept, and where it goes
Retention is enforced by scheduled sweeps in server/taskworker, not by policy. The windows below are read from the constants those sweeps call, and they are shorter than the privacy policy — which states no retention period at all — would lead a reader to expect.
| Data | Kept for | Enforced at |
|---|---|---|
| Connection rows, and the per-connection city/region/country, latency and speed that hang off them | 8 hours | taskworker/work/network_client_work.go:91; the delete cascades network_client_location in the same statement (model/network_client_model.go:2580) |
| A top-level client idle (no auth, no connect) → marked inactive | 30 days | TopLevelClientIdleExpiration (model/network_client_model.go:2576) |
| An inactive client → hard delete, with cascades | +30 days | NetworkClientReapAfterDeactivate (:2556) |
| So an unused device, end to end | ~60 days | the two chained |
| Completed contracts | 7 days | taskworker/work/subscription_work.go:21-43 |
| Audit rows | 180 days | model/audit_model.go:1018,1052 |
Two notes an auditor should have. The idle window was tightened from 90 days to 30 on 2026-07-18; the constant carries its own rationale, and a stale comment at taskworker/work/network_client_work.go:83-91 still says 90, which is the likeliest source of that figure if you meet it elsewhere. And RemoveLocationLookupResults is scheduled every cycle but is a no-op — its body and its model call are both commented out and no such table exists. It is a vestigial stub, not unswept data; per-connection location is deleted with the connection row above.
Geolocation never leaves the machine. The city/region/country lookup reads the bundled MaxMind GeoLite2-City database, mmdb/geolite2.mmdb, from local disk (ipDb in server/ip.go), and ip.go contains no HTTP client and makes no network call. The operator refreshes that file with MaxMind's geoipupdate client, a download that sends MaxMind nothing about any user. This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com. GeoLite2 incorporates GeoNames data, available under CC BY 4.0. No address is sent to a geolocation service, and no personal information is shared with any third party. The only third parties in this document's parties table hold data the user hands them directly by choosing that path: the DoH resolvers a user's own DNS queries reach through the tunnel, and the payment processors a user signs up with to be billed.
The ingress LB does not log client addresses, and that is checkable. The load balancer's nginx configuration is generated by warp, which is open source under Apache 2.0 (warp/LICENSE), so this is source anyone can read rather than a deployment assertion. Two layers:
- The access log uses a purpose-built format that omits the address —
log_format noclientaddrcarries time, connection, host, request, status, bytes, timings, upstream and user-agent, and the config comments name what is deliberately absent:$remote_addr, and also$http_x_forwarded_forand$http_referer, "both [of which] can carry a client address forwarded from elsewhere" (warp/warpctl/config.go:1243-1254). - The error log cannot be formatted, so nginx's own
, client:field is removed downstream instead: the lb runs nginx with its stderr wrapped in aClientAddrScrubber(warp/nginx.go:98-103) that strips that field by regex and then replaces every remaining IPv4 or IPv6 literal anywhere in the entry with[scrubbed], keeping the port (warp/nginx.go:111,116,176-232). Nothing is exempted by range, so no rule about which addresses belong to users can be wrong.
A regression test pins it: TestNginxLogsOmitClientAddr (warp/warpctl/config_test.go:1208) fails if any access_log names no format, because nginx's built-in "combined" fallback leads with $remote_addr.
The service containers are scrubbed at the descriptor, not at the call site. warp runs service containers with --log-driver=journald (warp/warpctl/run.go:200) and does not filter their output, so for a time any line a service wrote with an address in it reached journald as written. The call sites were real and several: server/http.go:413,453 construct http.Server with no ErrorLog, so the Go standard library writes http: TLS handshake error from on every half-open connection — the hole proxy/http.go:199,273 closes with ErrorLog: discardLog; client-supplied SNI is logged at ERROR (server/tls.go:111); a raw caller address at server/proxy/proxy_device.go:858; WireGuard peer identity at server/proxy/server.go:699.
Those are no longer the boundary, because the boundary is no longer the call site. Each service replaces its own stdout and stderr file descriptors with pipes at the first statement of main and scrubs everything written to them (server.ScrubProcessLogs, server/scrub.go:62, called from all eight services — server/cli/{api,connect,alt,proxy,taskworker,gossip,monitor,mcp}/main.go). Operating on the descriptor rather than the writer means it covers log, glog, net/http's default ErrorLog, any dependency, and any call site added later, without anyone having to find them. It uses the same warp.ScrubAddrs (warp/nginx.go:207) the lb uses for nginx, so there is one implementation of the scrubber rather than two that can drift.
Two deliberate limits, both of which an auditor should weigh rather than take on trust:
- Crash dumps pass through unscrubbed. A
panic:,fatal error:,signal SIGorgoroutineline latches the scrubber into passthrough for the remainder of that process's life (scrubPassthroughMarkers,server/scrub.go:48). A dump is rare and is the most valuable thing in the log when it happens, so it is kept whole — accepting that a stack frame or register can carry an address. The latch writes a visible notice when it trips, so a process that stopped scrubbing is not something an operator has to infer. - It scrubs what parses as an address. Version strings and colon-separated identifiers that parse are redacted too. The most visible consequence is that
server/monitor/tailer.go, which scrapesip:portout of log lines, now sees[scrubbed]:443.
So "a service does not write your address to its logs" is now a property of the process rather than of its call sites. What this does not establish is what journald then does with what it receives; §12 records that as open. Full detail on the original call sites: review/verified/PRIVACY-ENFORCEMENT.md §2.4.
What the operator controls that is easy to overlook. Provider discovery is entirely operator-side: FindProviders2 is the only live endpoint (server/api/api.go:140), the platform scores and ranks candidates (server/model/network_client_location_model.go:5056), and the client takes the ranked set it is given. The only consumer-side control is a location block list (server/api/api.go:149). A client has no way to verify that the providers it was offered are independent of each other or of the operator.
6. Operator-run providers, and operator–provider collusion
6.1 Can the operator run providers?
Yes, and nothing in the system marks or prevents it. Providing is open to anyone with no attestation or stake, and there is no first-party, official or operator-owned provider concept anywhere in the code — searches across server, connect and sdk for such a notion return nothing. An operator-run provider would be indistinguishable from a member's.
Combined with §5's point that the operator ranks and returns the provider set, this means the operator can, in principle, place its own providers in a user's window. There is no client-side diversity check, no attestation of independence, and no published measurement of the fleet by an outside party. The counterweights that exist are real but partial: the window holds several providers at once — a quality window of 2–6 (hard cap 12) and a speed window of 1–2 (hard cap 4), both live in the default profile (connect/ip_remote_multi_client.go:160-189) — and providers are removed on unhealthy stats, blackhole detection, ping failure and per-channel lifetime rotation (:37-54, :610-616). Those bound how much any one provider sees. They do not bound a set of providers chosen by one party.
6.2 What operator and provider reconstruct together, per mode
Assume the operator and one or more providers in your window share what they each hold.
| Relayed standard | Relayed sealed | Direct | |
|---|---|---|---|
| Your identity (account, email/wallet/payment) | operator | operator | operator |
| Your real IP at connect time | operator | operator | operator, and the provider directly |
| Destinations you visited | operator (inner packets) and provider | provider only | provider |
| Link between the two | trivial — one party already holds both | the contract: audit_contract_event pairs client and provider identity per contract (server/db_migrations.go:325-347) | trivial |
| Result | full browsing attribution for whatever the colluding providers carried | full browsing attribution for whatever the colluding providers carried | full browsing attribution, plus the provider knows your address unaided |
The honest conclusion: the sealed session does not defend against operator–provider collusion. It removes the operator's independent ability to read your traffic; it does not stop a provider that already sees your destinations from handing them to an operator that already knows who you are. The contract record that joins the two is not a leak — it is the accounting the network runs on.
What the seal does buy against this adversary is a scope limit: with it on, the operator's own view contains no destinations, so collusion requires the cooperation of the specific providers that carried the specific traffic, at the time it was carried, rather than a query against records the operator holds alone. That is a meaningful difference in effort and in what compelled disclosure can reach retroactively (§10). It is not immunity, and no arrangement of three parties where one selects the others provides immunity.
The design's structural answer, which is not deployed. The wire protocol and the client support chaining additional provider intermediaries — MaxMultihopLength = 8 (connect/connect.go:15-18), IntermediaryIds on CreateContract (connect/protocol/transfer.proto:547-561), client acceptance at connect/ip_remote_multi_client_api.go:661-662, server plumbing at server/controller/connect_controller.go:915. The live discovery endpoint never populates the field — FindProvidersProvider is constructed at exactly two sites and neither sets it (server/model/network_client_location_model.go:5044,5093; the field is declared at :3693 and assigned at neither) — so the shipping network assigns chains of length one. Provider chaining is a capability of the protocol, not a property of the deployment, and quoting the number 8 as a hop count would be misleading.
7. Provider identity keys, and whether the operator can substitute one
This is the section most worth attacking, because the answer is uncomfortable and the source says so itself.
7.1 Issuance and binding
Every connect.Client — a provider client, and each of the user's window clients — holds an Ed25519 keypair generated in-process by ClientKeyManager (connect/transfer_key.go:87-143, the only ed25519.GenerateKey call in the three trees). It is long-lived where a seed is persisted and reloaded, which is the provider client, and fresh per process where none is, which is the window clients; §9 covers the difference and why it matters. The private half never leaves the process; a wrong-size seed is a hard construction error rather than a silent fresh identity (:84-94). The public half is published to the platform in a ClientKey control message (connect/protocol/transfer.proto:684-700) and stored server-side in Redis at ckey_ with no expiry — Redis is the source of truth, there is no SQL table (server/model/network_client_key_model.go:19-65).
Binding is therefore client_id → public key, and the operator issues the client_id and holds the mapping. There is no external anchor: ids are not derived from keys, and there is no transparency log or DHT. Both alternatives are recorded in the design as considered and deferred (connect/DESIGNNOTES.md §3.7, "Deferred alternatives").
7.2 Rotation and revocation
Republishing overwrites — SetClientKey's own comment reads "keyed on client_id (rotation overwrites)" (server/controller/connect_controller.go:1193-1199). The entry is deleted when the client id is reaped (network_client_key_model.go:67-78, called from RemoveDisconnectedNetworkClients). There is no scheduled rotation, no expiry, and no revocation list. A provider client persists its key material and reloads it, so its identity is stable across restarts (sdk/device_local.go:3592-3597; sdk/local_state.go:788) — and, on Android, deliberately stable across an automatic logout wipe as well (§9.2). A client with no persisted seed generates a fresh identity each process start, which is what the user's window clients do. Mid-session key changes are refused: SetPeerClientPublicKey is first-write-wins, and a differing later key is logged and ignored (connect/transfer_encrypt.go:2051-2096).
7.3 Verification, and the two defences
- Defence 1, signed cert binding. The provider signs its ephemeral TLS cert chain with its Ed25519 key and publishes both (
connect/protocol/transfer.proto:494-522). The platform attaches the chain, the signature and the provider's public key to every contract naming that provider (:340-368,:388-406). The client admits the chain only if the signature verifies under the provider's public key, and then checks the cert presented in the handshake against the admitted chain (connect/transfer.go:9560-9583, checked at:11928). - Defence 2, in-handshake identity proof. After the TLS handshake each side signs the RFC 5705 exporter output under label
urnetwork-sequence-identity-proofand sends it as anEncryptedControl(connect/protocol/transfer.proto:615-660,701-712). The AEAD is withheld from the wrap path until the peer's proof verifies (connect/transfer_encrypt.go:1719-1726,:1719-1756), so a MITM who terminates TLS on one leg and re-handshakes on the other produces mismatched exporters and cannot forge the signature.
Both defences verify against peerClientPublicKey — and that value is taken from the platform-authored contract (connect/transfer.go:9560-9583; connect/transfer_encrypt.go:2051-2096).
7.4 Can the operator substitute a provider key? Signed, attributable, and much harder — but not impossible.
Updated 2026-09-17. This section previously read "Today: yes." That is no longer the whole answer, and the change is the second-most significant hardening in this document's history after the fail-closed fix.
The attack, unchanged in shape. An operator that substitutes the certificate, the certificate signature and destination_client_public_key in lockstep defeats both defences in §7.3, because both verify against a key the same contract supplied. The design notes state it in the repository's own words (connect/DESIGNNOTES.md §3.7).
What now stands in the way. With Post Quantum Encryption on (EncryptionModeRequired), the client withholds the session cipher until the contract-supplied identity key is corroborated against a signed registration history the operator has published, and a verified disagreement is terminal for that provider — no traffic flows to it and it is dropped from the window.
Each registration (connect/transfer_key_history.go:200-213) carries the deployment domain, a monotonic generation, the identity key, a link to its predecessor's content hash, and a secp256k1 signature by the operator's root signer. The client checks canonical encoding, signature recovery, chain contiguity from generation 1, domain and signer authority, and forward-only movement against a pinned head (VerifyClientKeyHistory, :422). The gate lives in Cipher() (connect/transfer_encrypt.go:2699-2716) and the decision table in connect/transfer_key_history_session.go:157. Server side: server/model/st_client_key_history.go stores the signed chain, server/controller/st_client_key_history_public.go:77 serves it, routed at server/api/api.go:226.
So the operator can no longer be inconsistent and deniable. To substitute, it must commit in signed form to a chain, and that artifact is permanent, attributable, and divergent from what every other reader of that client id sees — including the independent validators already auditing this log. The older unsigned cross-check against GET /key/ still runs alongside and is still advisory, logging CONTRACT vs FETCHED ... MISMATCH ... (today: log only) at connect/transfer_encrypt.go:2131-2134.
Four precisions an auditor should hold onto.
- Signed is not unforgeable-by-the-signer. An operator malicious from your first contact and self-consistent about it signs one coherent chain naming its own key. Verifying the chain checks the chain, not the authority behind it; the protocol says so itself — "The caller must separately authenticate the expected signer in the operator's pinned chain version" (
sn/protocol/client_key_history.go:166-167). The client answers with build-pinned signers plus trust-on-first-use (connect/transfer_key_history.go:491), which catches an operator that turns malicious, targets a subset, or forks — and not one that was never honest. Closing that requires the plurality read across independently operated operators, which is deferred (connect/DESIGNNOTES3.md§10). - Omission is still the cheaper move, and the ratchet is what bounds it. Certificate verification is skipped without latching when the trusted set is empty (
connect/transfer.go:11915-11926), and the identity proof cannot be verified at all when the peer's key is absent (connect/transfer_encrypt.go:1947-1950). Withholding the signed history is the same move one level up. It is bounded by a tier ratchet: a peer once verified against signed evidence is never again accepted without it, and once an operator has served signed evidence to an install, a peer with none is refused (connect/transfer_key_history_session.go:168-185, persisted atsdk/peer_client_key_pin_store.go). A fetch error is deliberately not treated as a rejection — otherwise one API outage would exclude every provider for every user at once — so an operator that breaks its own endpoint degrades clients to the contract key. That is an accepted availability trade and a real residual. - All of it is scoped to the toggle. Enforcement runs only under
EncryptionModeRequired(connect/transfer_key_history_session.go:91), because refusing under Opportunistic degrades to plaintext, which is what a substituting operator wanted. With the toggle still shipping off (§2.2), a default-configuration client gets none of this. - Contract authenticity is rooted in the operator too. A provider verifies the contract HMAC using a provide secret key that the provider generates and publishes to the platform (
server/controller/connect_controller.go:727,840;sdk/device_local.go:3592-3597). The operator holds a copy, which is what lets it author contracts at all. That is expected of an accounting authority, but it means "the contract is genuine" is not an operator-independent statement.
What this does and does not undermine. It does not touch provider blindness to identity, which depends on no key distribution. Operator blindness to content on the sealed path is no longer a bare policy property: substitution now costs the operator a signed, permanent, third-party-detectable artifact. It is still not a property that holds against a determined operator that was never honest, and any URnetwork document describing the sealed session as making operator blindness unconditional is overstating it. This document supersedes such wording.
8. Timing and traffic correlation, and the global passive observer
8.1 There are no traffic-analysis defences. None.
URnetwork ships no cover traffic, no padding, no mixing, no batching delay and no traffic shaping on user data. This was checked exhaustively across connect, sdk and server:
- No chaff, decoy, dummy or cover-traffic generator exists anywhere.
- The AEAD is length-preserving by construction: ciphertext length is nonce + plaintext + tag (
connect/transfer_encrypt.go:303-336). No protobuf inconnect/protocol/carries a padding field, and framing is a bare 4-byte length prefix (connect/message_framer.go:29-31). - Send-side coalescing is explicitly zero-delay: "There is no batching wait: the sequence only takes a second Pack when it is already queued" (
connect/transfer.go:2971). Everyjitterin the trees is reconnect backoff or channel-lifetime spread, not traffic shaping. Ack compression (connect/transfer.go:272) delays acknowledgements only, to cut relay message volume. - The one place user traffic is paced at all is the DNS-shaped transport's
WritePacketsPerSecondcap (connect/transport_pt.go:68,359-361), which exists to be resolver-friendly and applies only to the least-preferred transport mode. Its idle "pump" queries are a distinguishable length from data-bearing ones, so it conceals neither size nor rate.
Therefore: an adversary who observes traffic entering your device and traffic leaving the providers that carry it can correlate the two by size and timing. URnetwork does not defend against that adversary. This is the same statement Tor makes about end-to-end correlation, and it applies here with less margin, because URnetwork's design goal is low latency — which is exactly the property that makes correlation easier. A latency-bounded four-leg path is a deliberate trade against a mixnet's added delay, and this is the side of that trade the user pays. Counting the extender leg does not change it: that hop forwards the encrypted session on to the operator without terminating it, so it adds no layer for an observer to strip and no delay for one to lose you in.
Two things are sometimes mistaken for defences and are not. Extenders and the shaped transports (connect/net_resilient.go:114-215, connect/transport_pt.go:37,73) target DPI-based blocking, and the resilient layer disables itself once the stream is established (net_resilient.go:102,146-161) — it never touches data-phase records. TLS session-ticket caches are deliberately not shared between egress paths so a server cannot link them through a redeemed ticket (connect/net_tls.go:38-42,99-103); that is ticket unlinkability, not traffic analysis.
Nothing in the source acknowledges traffic correlation as a limitation: a search for traffic analysis, timing correlation, traffic correlation and global adversary across all three trees returns zero hits. The in-repo threat model (connect/DESIGNNOTES.md §3.7) is entirely about the operator MITM. This document is the first place the gap is written down.
8.2 The multi-provider window is not an anti-correlation mechanism
Traffic normally exits through several providers at once — commonly three to eight across the two windows — and per-site affinity keeps a given site on one provider (connect/ip_remote_multi_client.go:160-189,837). That genuinely limits how much any one exit sees. But the window's design rationale in the source is reliability throughout — bad-destination mitigation, health-weighted resizing, blackhole detection (:27-52) — and the one comment reading "lower affinity is more private" sits on ClientAffinityTimeout, a knob that is commented out (:587-591, and again in the defaults at :220), with DestinationAffinity shipping true for reliability reasons (:270). Plural exit identity is a real benefit and it is fair to claim; claiming it as a designed defence against correlation is not supported by the code.
8.3 The global passive observer: out of scope
A global passive adversary — one able to observe a large fraction of internet links simultaneously — is out of scope, and URnetwork provides no defence against it. With no padding and no cover traffic, such an adversary correlates flows across the relay and the providers directly. This is the same position Tor takes, and unlike Tor, URnetwork does not even add per-hop delay that would raise the cost.
What is genuinely different, and is worth stating without inflating it: the egress set is a churning population of independent residential connections rather than one operator's published address ranges, so an adversary must observe a wider and shifting set of endpoints to cover a single user, and the endpoints cannot be enumerated from a server list. That raises cost. It does not change the outcome for an adversary who can already see both ends.
9. Device identifiers and linkability
9.1 What a provider can observe about you
The contract's SourceId is a per-window-slot client id, not your device. StoredContract.SourceId is the authenticated caller's client_id (server/controller/connect_controller.go:816-830; parsed by the provider at connect/transfer.go:6088-6106). The generator mints a fresh client id, jwt and instance id for each window entry and removes it on teardown — the source says so directly: "The api generator mints an ephemeral platform client id (with a fresh instance id) for every window entry and removes it on teardown" (connect/ip_remote_multi_client_identity.go:10-15; minted through AuthNetworkClient at connect/ip_remote_multi_client_api.go:691-727, released through RemoveNetworkClient at :802-832). Window membership churns on top of that: providers leave on unhealthy stats, blackhole detection, ping failure and per-channel lifetime rotation (connect/ip_remote_multi_client.go:37-54), so a slot id is short-lived by construction rather than by policy.
Your traffic is spread across several providers at once, so no one exit sees the client. The default profile runs a quality window of six and a speed window of one to two (connect/ip_remote_multi_client.go:160-189). Each provider holds a slice of your activity under its own slot id, and per-site affinity keeps a given site on one provider rather than smearing it across all of them (:837). This genuinely bounds what a single honest-but-curious exit can reconstruct. It is not an anti-correlation defence and §8.2 says why.
The window ids and your provider-role id are separate identity spaces, so a provider cannot reverse a slot id back to you. If you also share your connection, your provider client is the top-level client holding the durable, persisted key seed (§9.2). The window clients are different clients: built from a fresh DefaultClientSettingsWithBufferSize (sdk/device_local.go:4349) that carries no ClientKeySeed, so each generates its own keypair per process, and your own client id is explicitly excluded from your own candidate set (sdk/device_local.go:4338). A provider that holds a slot id and looks it up via the unauthenticated GET /key/ therefore gets a key that is fresh for that process and links to nothing durable — not to your provider identity, not across restarts, not across slots. The durable publicly resolvable key in §9.2 belongs to the provider role, and nothing on the egress path exposes it.
The device's own client id does not reach a provider on the egress path, and the trace is short enough to check. DeviceLocal.sendPacket does set source := connect.SourceId(self.clientId) — the device's top-level id — and hands it to the multi-client (sdk/device_local.go:4870, batch form at :4968). That value is used only for local bookkeeping: keying egress fragment reassembly and the provideMode relationship check (connect/ip_remote_multi_client.go:5876-5906). The wire send is made by the per-slot window client — self.client.SendMultiHopWithTimeoutDetailed(frame, self.args.Destination, …) (connect/ip_remote_multi_client.go:14941-14948), on a Client the generator minted for that slot (:13094) — and the device source id is not among its arguments. A grep for source across that send region returns nothing.
The one identity that is durable is the provider role, and it points the other way. A device that shares its connection runs a second client built at sdk/device_local_provider.go:198 with ProviderStreamPolicy = true (:191) and the persisted key seed. Network-peer relationships and provider-side return traffic ride that client, so they carry the stable top-level client_id and the durable key. That is the identity a device serves under, not the identity it browses under: it is exposed to the consumers a provider serves, which §9.2 documents in full, and it is never handed to the exits that carry a consumer's own traffic. The two identity spaces are the ones described above, and they do not cross.
But those identities are deliberately reused for up to four hours, on every path — not only hosted ones. The device installs its own local identity store unless it is hosted (sdk/device_local.go:824-830), and a stored window identity is replayed against the same destination until it goes stale: windowIdentitiesStaleAfter = 4 * time.Hour (sdk/window_identity_store.go:42-45). The purpose is to keep a provider's NAT flows alive across a restart (connect/ip_remote_multi_client_identity.go:17-23). The cost is that a provider can see the same SourceId reappear from you across app restarts within that window. Say four hours, not "ephemeral".
The client Ed25519 identity key is fresh per window client, on the egress path. Window client settings are built from a fresh DefaultClientSettingsWithBufferSize (sdk/device_local.go:7105), which carries no ClientKeySeed (connect/transfer.go:1547-1558), so ClientKeyManager generates a new keypair (connect/transfer_key.go:113). The persisted window snapshot stores client id, jwt and instance id only — no key seed (sdk/window_identity_store.go:42-45), so a restored identity republishes a new key. With Post Quantum Encryption off, no identity proof is presented at all (connect/ip_remote_multi_client.go:13080-13091).
The tunnel source address is per session and low-entropy. The SDK assigns its TUN a random RFC1918 address — "a random 10.x.y.h (RFC1918, DHCP-shaped)" (sdk/device_local.go:7105) — generated with a host octet in 2..254 over the smallest non-conflicting 10.a.b.0/24 (connect/tun.go:517-523). It is computed in the constructor and never persisted, so it changes each session. The provider does see it: it is the source field of every tunnelled packet and the provider keys NAT state on it (connect/ip.go:731,784). Roughly 253 values is a weak per-session fingerprint, not a cross-session identifier.
9.2 The provider-role identity key is durable
If you also share your connection, your provider client holds a long-lived Ed25519 keypair whose seed is persisted to local storage (sdk/local_state.go:788, .device_local_key_material, JSON client_key_seed; applied at sdk/device_local_key_material.go:32,90). Unlike the window clients, that key is deliberately preserved across an automatic logout wipe — the Android comment is explicit: "Clear a stale or partial auth state WITHOUT rotating the device identity. The identity key material is device-scoped, not session-scoped" (android/.../MainApplication.kt (clearStaleAuthState)). Token refresh, partial-auth recovery and the 30-day idle re-login therefore produce a new client_id and a new device_id while the Ed25519 public key stays the same. Only an explicit user logout rotates it (sdk/local_state.go:788).
That key is published, sealed into every contract naming this client as destination (server/controller/connect_controller.go:744-770,816-830), and readable by anyone: GET /key/ is unauthenticated by design (server/api/api.go:222-223; server/controller/connect_controller.go:1227). So any party — not just a provider you served — can resolve any client id to its public key and cluster the client ids that share one. For a provider account, that is a durable cross-session, cross-client_id, cross-device_id handle. This is a real cost of providing, and it is not documented anywhere else in the corpus.
9.3 At the operator, everything links
client_idis never revoked. The code's own comment: "client_ids are globally unique addressess tantamount to IPv6 / they are never revoked once allocated, to preserve security and audit records" (server/model/network_client_model.go:74). A top-level client is deactivated after 30 days idle (TopLevelClientIdleExpiration,:2576) and hard-deleted 30 days after that (NetworkClientReapAfterDeactivate,:2556), cascading to the device row and the Redisckey(:2537).device_idis per login, not per hardware or per install: a fresh one is minted with each top-level client (:332-352), and the code notes the resulting "identity churn (a fresh device_id per login)" (:2280). Window clients inherit it (:356-380) and it is never sent to a provider.- The network token is a 24-hour JWT (
server/jwt/by_jwt.go:55-67,238), refreshed by the SDK at half-life — about 12 hours — with jitter (sdk/device_token_manager.go:172-217). Refreshing the token does not rotate the client id; the same claim is re-minted (server/model/network_client_model.go:74). audit_contract_eventrecords client and provider identity per contract (server/db_migrations.go:325-347).client_reliabilityjoins the keyed IP-block hash directly to a client id, per block: the original four-column primary key (server/db_migrations.go:325-347) was later reduced to(block_number, client_address_hash, client_id)(:2191-2195), and the live partitioned table uses the same three (server/model/network_client_reliability_partition_model.go:194), withnetwork_idsurviving as an index payload (:63,631-634). Retention is 30 days (server/model/network_client_reliability_partition_model.go:52-69; daily partitions dropped whole).network_client.auth_timeis a durable last-seen retained 30 days (server/model/network_client_model.go:2556,2576); disconnected connection rows are reaped at 8 hours (server/taskworker/work/network_client_work.go:85).- Account-level identifiers, at the operator only: cleartext email or phone in
network_user.user_auth, the full third-party IdP JWT for SSO logins, cleartext identifiers on every login attempt inuser_auth_attempt, wallet addresses, payment rows, a persistentnetwork_referralwho-invited-whom edge, andaudit_provider_eventrows carrying literal country, region and city strings againstnetwork_idanddevice_id, not tied to the 8-hour connection reap (review/verified/PRIVACY-ENFORCEMENT.md§1.4). User-Agentis logged by the five-header allowlist (server/http_log.go:13-29). It is the one entry in the five that is a fingerprinting surface.
9.4 The WireGuard bridge: the tunnel address is NAT'd before egress
Each WireGuard client is allocated a stable private tunnel address from a shuffled pool of 10,000,000 RFC1918 addresses (server/model/network_client_proxy_model.go:774), written into the config as the interface address (:756-771). That address is durable: it is part of the peer's config and survives every session.
It does not reach the providers. The hosted device substitutes a per-device address on egress and restores the peer's own on return. The substitute is taken once at device construction from the same 169.254.0.0/16 pool the app path's tun uses (server/proxy/proxy_device.go:954, via connect.TakeLocalIpv4Address, connect/tun.go:471) and returned on close, so it is per device rather than per account, and it is drawn from the same space as every other local tunnel address rather than being distinctive. The rewrite is natRewriteEgress (server/proxy/proxy_device.go:1297) and natRewriteReturn (:1314), over connect.RewriteIpv4Source/RewriteIpv4Destination (connect/ip_nat_rewrite.go:71,77), which repair the IPv4 header checksum and any pseudo-header transport checksum incrementally. Return packets are matched on the substituted address, since that is what the provider addressed them to (receiveWithNotifyNat, server/proxy/proxy_device.go:1339).
Three limits worth stating, because this removes one linkage and not others. Only the attached peer's own traffic is rewritten, so a device also serving HTTP or SOCKS leaves those flows untouched. If the local pool is exhausted the rewrite disables itself rather than dropping traffic, which means the old behavior is still the failure mode — an availability choice, and the reason the property is "normally holds" rather than "cannot fail". And every provider in one window still sees the same substituted address for the duration of that device, exactly as the app path's tun address is shared across a session's providers (§9.1); what is removed is the cross-session, account-durable handle, not within-session correlation.
Behavior pinned by TestWgNatReplacesThePeerAddressOnEgress, TestWgNatRoundTripIsExact, TestWgNatLeavesOtherSourcesAlone and TestWgNatReturnIsMatchedOnTheNatAddress (server/proxy/proxy_device_nat_test.go), and the checksum arithmetic by twelve tests in connect/ip_nat_rewrite_test.go that verify against full recomputation rather than reproducing the incremental math.
Combined with the fixed 1.1.1.1 resolver (§3.2) and the absence of a sealed session on that path (§2.4), the WireGuard bridge remains the least private way to use the network. The apps and SDK are the most private.
10. Legal demands
Jurisdiction. The counterparty is BringYour, Inc., a Delaware C corporation (docs/legal/terms.md:8-9 names the entity; the incorporation is not stated there), with a notice address at 2261 Market Street #5245, San Francisco, CA 94114 (docs/legal/terms.md:265-271). Governing law is Texas and venue is Harris County, Texas (docs/legal/terms.md:314-321) — a deliberate choice, and not an inconsistency with the California address: the company's counsel is in Texas. The one gap a reader hits is that the published terms state neither the incorporating state nor a registered agent, so the corporate facts needed to serve process cannot be established from them alone. All of it is US, and all of it is reachable by US compulsory process, including process that arrives with a gag order. The terms state that the operator "may monitor and disclose information where required to do so by law or government order" (docs/legal/terms.md:204-206).
What a demand on the operator reaches. In descending order of deanonymising power: who the account is — cleartext email or phone, the third-party IdP JWT for SSO logins, wallet addresses, and payment records that tie to a real identity via Stripe, Apple, Google Play or an on-chain tx_signature; when it connected and from which city, per connection; which providers it used and for how many bytes; and a keyed hash of the /29 or /56 block it connected from, plus the cleartext source port. Third parties hold more: the payment processors named in §1 hold identity the operator never stores, and a Solana tx_signature resolves to a wallet on a public chain.
What a demand does not reach, because it does not exist. The transfer ledger has no destination, host, URL, SNI, port or domain field anywhere (review/verified/PRIVACY-ENFORCEMENT.md §1.2). Uploaded support logs are discarded on receipt. There is no browsing record to produce, on any path, sealed or not — this is the strongest structural property in this document, and it holds whether or not anyone behaves.
What a demand reaches prospectively. Everything above is about stored records. A demand that compels future conduct is a different matter, and §7.4 is where it bites: an operator compelled to intercept a specific user could, today, substitute or omit provider key material and read that user's traffic even with Post Quantum Encryption enabled, with the only signal being a device log line. The sealed session raises the cost of retrospective disclosure. It does not, in its current form, resist a compelled operator going forward.
Account deletion is partial. RemoveNetwork is a hand-written delete list, not a database cascade (server/model/account_model.go:108). It removes the network_user rows and their auth records (password, SSO, wallet, seedphrase), the network row and the name index entry, and it schedules a Stripe unsubscribe. It does not delete account_payment, stripe_customer, apple_subscription_transaction, completed solana_payment_intent rows, network_referral, device rows or audit rows; those age out on their own schedules — audit events at 180 days (server/model/audit_model.go:1018,1052) — or, for completed on-chain payments, not at all (server/model/solana_payment_intent_model.go:96,137 deletes only intents with a null tx_signature). Deletion also writes a row: an AuditEventTypeNetworkDeleted event keyed on the deleted network_id (account_model.go:108). The privacy policy's "delete their account and associated personal information" (docs/legal/privacy.md:69) is broader than what the code does.
There is no warrant canary, no transparency report, and no published law-enforcement process. Verified absent across docs/ and the ur.io site source. There is no legal-process address at all: [email protected] is scoped to vulnerability reports (docs/legal/vdp.md:42) and [email protected] is the general contractual notice address (docs/legal/terms.md:265), neither of which is a channel for service of process. A party wishing to serve the company has nothing in the published terms to act on beyond the San Francisco notice address. URnetwork's own comparison documents already concede the missing canary and transparency report against competitors that publish one, and this document repeats it rather than softening it.
The privacy policy is silent where it should be specific. It states that "all personal information that we collect from and about users is limited to e-mail address or telephone number" and that collection happens "directly from you when you provide it" (docs/legal/privacy.md:29-37).
An earlier draft of this section called that an under-description on the ground that the code stores more categories than the policy names. That framing was withdrawn on 2026-08-09 as wrong. Everything else this document inventories is either account data the user creates by signing up or paying (the Stripe join key exists because someone agreed to be billed), or it is not personal information at all: a source port, and a keyed block hash the operator does not reverse. Whether a keyed hash could in principle be inverted by its own holder is a real engineering caveat — §5 states it, and the missing rotation is a genuine weakness — but a value nobody looks up is not a category of personal information the policy failed to declare.
What the policy is actually missing is different and narrower: no retention period, no logging statement, and no law-enforcement or government-disclosure section. The strings retention, log, IP address, subpoena and legal process do not appear in it. That matters because the retention discipline exists in code and is stronger than the policy claims — §5.1 sets out the verified windows. A user reading the policy today cannot hold the operator to any of them.
11. What URnetwork does not defend against
Three things hold, and they are worth naming before the list that follows, so the list is read as a boundary rather than a verdict. On a relayed path a provider never learns who you are, and no setting or provider build changes that (§2). There is no destination, host, URL, SNI, port or domain field anywhere in the transfer ledger, so there is no browsing record to compel, leak or sell (§5, §10). And every one of these claims is readable in public source, which is why this document can be specific about its own failures.
Everything below is a failure. Stated without hedging; each item is expanded above.
- End-to-end timing and traffic correlation. No padding, no cover traffic, no mixing. An adversary observing both your access network and the providers carrying your traffic can correlate them. §8.1.
- A global passive adversary. Out of scope entirely. §8.3.
- Operator–provider collusion. The operator knows who you are, the provider knows where you went, and the contract record joins them. The seal narrows what the operator holds alone; it does not break the join. §6.2.
- An operator that was never honest, substituting provider key material. Substitution now costs a signed, permanent, third-party-detectable artifact, and an operator that turns malicious or is inconsistent is caught. One that is malicious and self-consistent from your first contact still wins, because the client has no operator-independent view of which signer is authoritative. §7.4.
- No seal at all WITH Post Quantum Encryption OFF, the shipping default. In that mode the client never starts a session, so its traffic is unsealed from the start, with no user-visible indication. Fixed for the mode that asks for it, 2026-08-10: with the toggle on, the client runs fail-closed and refuses to send or accept plaintext application data. What survives in both modes is the missing indicator — no app shows whether a given connection is sealed. §2.2.
- A malicious provider tampering with unencrypted traffic. Plaintext HTTP is passed through unchanged; the provider is in the position of a hostile hotspot. §3.
- Endpoint compromise. Malware, a compromised OS, a hostile browser extension, or anyone with your unlocked device. Nothing in a network product addresses this.
- Destination-side identification. Cookies, logins, browser fingerprinting and account behaviour identify you to the sites you visit regardless of how the packets arrived. Changing your exit address does not make you anonymous to a service you log in to.
- Payment-rail identity. Card and app-store rails put your identity with the processor even though the operator does not store it. On-chain USDC leaves a public
tx_signature. - Sybil providers. Anyone can provide, with no attestation or stake. A single party can run many providers, including the operator.
- The browser and proxy paths' lack of a seal. The browser extension has no Post Quantum Encryption setting, and on those paths the operator runs the client. §2.4.
- Within-session correlation on the WireGuard bridge. The peer's durable tunnel address is NAT'd before egress, so it no longer follows an account across sessions — but every provider in one window still sees the same substituted address, exactly as they see one tun address on the app path. And if the local address pool is exhausted the rewrite disables itself rather than dropping traffic, so the pre-NAT behavior remains the failure mode. §9.4.
- A durable identity handle for anyone who also provides. The provider client's Ed25519 key survives client-id and device-id rotation by design, and
GET /key/is unauthenticated, so any party can cluster client ids that share a key. §9.2. - Traffic the local network sees during the DNS startup window. Documented in the source as an accepted cost. §3.2.
- Anything a third-party audit would have found. None has been performed on the protocol or the server code.
12. What this document could not establish
An unestablished claim in a threat model is a claim that should not be in the documentation either. These are open.
- Journald retention and forwarding on the hosts. Service logs are now scrubbed at the descriptor before they leave the process (§5), so what reaches journald should carry no addresses outside a crash dump. What this source review cannot establish is how long journald keeps what it receives, where it forwards it, and whether any crash dump — which passes through unscrubbed by design — is retained longer than the rest. That is a deployment question, not a code one.
- Whether name resolution on the browser path can ever happen locally. The extension configures an HTTPS CONNECT proxy by default, which resolves remotely, and SOCKS name resolution goes through the tunnel's DoH dialer (
connect/tun.go:517-523). Per-browser proxy DNS behaviour under every configuration was not tested. - Actual production verbosity.
BY_LOG_Vdefaults to 0 (server/env.go:46), which suppressesV(1)-gated destination logging on the client and server — but the only deployment manifest in the tree sets it to2(xops/gitops-unused/.../api/deployment.yaml:47-48), in a directory namedgitops-unused. Treat verbosity as a runtime flag, never a structural guarantee. - Fleet independence. The operator publishes live city and country counts from its own aggregate over currently-connected, valid providers (
server/model/network_client_location_model.go:3525), and location is derived by the operator from the connection it observes rather than declared by the provider (review/verified/ARCHITECTURE.md§6.4). But no independent party has measured the fleet from outside, and nothing verifies that the providers offered to a particular client are independent of one another or of the operator. The mechanism is checkable in source; the population is not checkable by a third party today. - The identity-key binding at account creation. The design states the expectation that "the (ClientId, public key) binding is registered at account creation" (
connect/transfer_key.go:60-85). What ships is a client publishing its key to operator-controlled Redis, served back by an operator-controlled API. Whether a stronger registration exists operationally could not be established from source; assume it does not. - Whether
RolesandPrincipalare ever populated for ordinary consumer clients. They are operator-assigned strings sealed into the signed contract bytes and set only forProvideMode_Network(connect/protocol/transfer.proto:547-561;server/controller/connect_controller.go:831-838). No evidence was found that the apps populate them, and not every caller was audited. If they were ever populated for consumer traffic they would be first-class cross-session identifiers visible to a provider. - Key-material persistence on non-mobile hosts.
ClientKeySeedis referenced only fromsdk/local_state.go,sdk/device_local_key_material.go,sdk/device_local.goandsdk/cgo/. Hosts that do not call those get a fresh key per process; the Linux, Windows, extension and server-proxy behaviour was not verified individually.
13. Reporting
Vulnerabilities: [email protected] and the disclosure policy at ur.io/vdp. Corrections to this document, including disagreements with any verdict in it, are welcome by the same route — an independent finding that contradicts a claim here is more useful than the claim.