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-08-07. 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.
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 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 ships on by default 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).
The integrity defects — one fixed, one open. The seal used to fail open silently in every mode. Fixed 2026-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). With the toggle off, the old opportunistic behavior still applies, and in neither mode does any app report which happened. Still open: can the operator substitute a provider key? Today: yes (§7.4), so the seal's confidentiality holds against a passive operator and an on-path stripper, not against an operator willing to author a false key. verify/BEFORELAUNCH.md items 8 and 9 track both; §2.2 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. Sections 1 and 2 define the parties, the three connection modes, and what each party sees in each mode. 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:2310-2327). 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:3932 RemoteUserNatProvider → LocalUserNat) |
| 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 (connect/net_extender.go:51-55, connect/extender/extender.go:57-61) |
| 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:150-155) |
| Payment processors | Stripe, Apple, Google, Solana/Circle | Hold real identity for paid accounts; the operator stores the join keys (server/db_migrations.go:1712-1719, :4640, :2350-2356) |
The operator is BringYour, Inc., a Delaware corporation with a San Francisco mailing address (docs/legal/ur.xyz/terms.md:16,25; docs/legal/terms.md:269-271). Section 10 covers what that means for legal process.
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 | the native default, out of the box; native apps only; 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 browser and proxy paths (§2.4), or the seal turned off; 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:4298-4303; connect/connect.go:45-49). 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 default, not a setting — 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). What is conditional is no longer whether the user finds the switch, and 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: ON. The product owner ruled on 2026-08-07 that the sealed session ships enabled, so operator blindness is the default rather than a choice the user has to make. This document is written against that launch state; at the time of writing the code still defaults off (verify/BEFORELAUNCH.md item 7).
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 now propagates 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; connect/transfer.go:1500-1509) — 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 (connect/transfer_encrypt.go:44-51). Key exchange is the hybrid X25519MLKEM768, with conventional X25519 as fallback (connect/transfer_encrypt.go:373-381,406); the AEAD is AES-256-GCM over a 32-byte exported key (:285-297); identities are Ed25519 (:892-897), so "post-quantum" scopes to the key exchange only. Mutual TLS is required and the peer certificate is checked at the sequence layer against the contract's commitment rather than by the TLS stack (:388-406).
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 handshake did not complete: a failed handshake, a failed identity proof, or a contract that never carried the peer's public key all leave it nil. Identity-proof failure is still non-fatal at the session layer — the session is "left unauthenticated" (connect/transfer_encrypt.go:1977) rather than torn down.
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:2796). An application pack waits for the cipher within the caller's timeout and is then refused, unsent — never downgraded — with a typedErrEncryptionRequiredNotEstablished. - Send backstop (
connect/transfer.go:4125). 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:5844). 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,EncryptionCapabilityPrefilter). 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. 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:9186-9197): 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:98) 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.
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, the opportunistic behavior described in earlier versions of this section still applies in full — traffic can flow in plaintext if a session never establishes, 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: completeHandshake's header at connect/transfer_encrypt.go:1650-1654 still asserts unqualified that "subsequent 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.
Still open: the user cannot see which mode a connection got. There is no per-connection sealed-or-not indicator in any app. The only signals are device-log lines — peer identity proof verified — cipher is now usable (connect/transfer_encrypt.go:1954) on success, an Errorf on failure, and a NotifyRequiredSendBlocked event when the gate refuses. The SDK exposes a change hook, so the missing piece is UI, not plumbing.
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:710-711). The flow is forced onto a P2P stream (connect/ip_remote_multi_client_probe.go:1176-1178) over a WebRTC/ICE data channel (connect/transport_p2p_webrtc.go:986), 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:1861-1863; sdk/local_state.go:607-616).
Two forced cases: same-network peers (your own devices) always allow direct (connect/ip_remote_multi_client.go:1822-1825), and hosted devices force it off (sdk/device_local.go:1516-1533). 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:556,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:128-131). 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:461-463) — 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:2430-2447), 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:1234-1240), 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:117-119, 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:554,604-624).
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:4481-4487), 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:870-876,6653-6683). The operator has no handler for that message type today, so nothing is stored on receipt (server/controller/connect_controller.go:236-250; 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:1120), 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:760), 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_profiles.go:14-30,50-57), and a DNS-shaped transport exists for networks where only DNS escapes (connect/transport_pt.go:18-45). These are mechanisms for crossing local and regional firewalls, ranked below the direct transports (connect/transport.go:541-546). 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:57-61). 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:51-55). 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. In the shipping client the direct transports are raced first and the extender dialers expand when those fail (connect/net_http.go:675); a client configured with custom extenders uses them as its only route (connect/net_http.go:357,469).
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 — server/ip.go:201 opens a bundled mmdb/ip-ipinfo.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 (server/model/network_client_location_model.go:1392-1414); 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-67). 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:40-44); 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-85; server/model/verify_model.go:111). The source port is stored in cleartext beside the hash (server/db_migrations.go:1934). The account-creation audit row records the peppered hash and port, never the raw ip:port (server/model/network_model.go:961-975), and audit rows are removed after 180 days (server/model/audit_model.go:984, scheduled at server/taskworker/taskworker.go:57,253).
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:15-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:13-16,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:84; the delete cascades network_client_location in the same statement (model/network_client_model.go:2295-2320) |
| A top-level client idle (no auth, no connect) → marked inactive | 30 days | TopLevelClientIdleExpiration (model/network_client_model.go:2289) |
| An inactive client → hard delete, with cascades | +30 days | NetworkClientReapAfterDeactivate (:2269) |
| So an unused device, end to end | ~60 days | the two chained |
| Completed contracts | 7 days | taskworker/work/subscription_work.go:161 |
| Audit rows | 180 days | model/audit_model.go:984 |
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 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 a bundled mmdb/ip-ipinfo.mmdb from local disk (server/ip.go:201), and ip.go contains no HTTP client and makes no network call. 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.
Known gaps in the "we do not log your address" posture, ungated by verbosity. server/http.go:415,455 construct http.Server with no ErrorLog, so the Go standard library writes http: TLS handshake error from to production stderr on every half-open connection — the exact hole that proxy/http.go:176,250 closes with ErrorLog: discardLog. Client-supplied SNI is logged at ERROR (server/tls.go:98,195), a raw caller address at server/proxy/proxy_device.go:274, and WireGuard peer identity at server/proxy/server.go:699. Until those are fixed, "we never log your IP" is not a claim URnetwork can make, and this document does not make it. Full detail: 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:67), the platform scores and ranks candidates (server/model/network_client_location_model.go:2250-2254,2430-2447), and the client takes the ranked set it is given. The only consumer-side control is a location block list (server/api/api.go:73-75). 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:138-158) — 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:234-251) | 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:13,214-233), IntermediaryIds on CreateContract (connect/protocol/transfer.pb.go:1516), client acceptance at connect/ip_remote_multi_client_api.go:296-312, server plumbing at server/controller/connect_controller.go:560. 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:3186-3190,3296-3303) — 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:76-100, 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:501-513) 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:14-59).
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:810-813). The entry is deleted when the client id is reaped (network_client_key_model.go:61-71, 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:2874-2889,2926-2935; sdk/local_state.go:418-461) — 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:1787-1816).
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:486-498). 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:3930-3934,4006-4018). - 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:460-476). The AEAD is withheld from the wrap path until the peer's proof verifies (connect/transfer_encrypt.go:1492-1498,: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:6119-6129; connect/transfer_encrypt.go:1780-1791).
7.4 Can the operator substitute a provider key? Today: yes.
Stated plainly: an operator that substitutes the certificate, the certificate signature and destination_client_public_key in lockstep defeats both defences and can machine-in-the-middle a sealed session. The design notes say so in the repository's own words (connect/DESIGNNOTES.md §3.7): "Residual hole: a platform that substitutes cert + signature + destination_client_public_key in lockstep still wins on the cert-binding side; the closing move is to feed SetPeerClientPublicKey from the OOB lookup rather than the contract. That cross-check is currently log-only."
The intended closing move exists and is wired, but does not act. An unauthenticated GET /key/ route serves the published key (server/api/api.go:123-124; server/controller/connect_controller.go:826-849), and the shipping SDK installs a per-session fetcher against it for every window client and every provider client (sdk/device_local_provider.go:398-420, reached from sdk/device_local.go:3434 and :90). On the first key set the client fetches the peer's key out of band and compares. On mismatch it logs:
CONTRACT vs FETCHED peer client public key MISMATCH for <peerId>
— possible platform MITM (today: log only, contract value still trusted)(connect/transfer_encrypt.go:1871-1876). The contract-supplied key continues to be trusted (:520-525, :1839-1849). Promoting log to refuse is described in both the settings comment and the design notes as the next hardening step — that is design intent, not a shipped property.
Three further precisions an auditor should hold onto:
- The out-of-band channel is out-of-band with respect to the contract pipeline, not with respect to the operator. The fetch goes to the same operator API host (
sdk/device_local_provider.go:404) and reads the same Redis the contract author writes. Even after log becomes refuse, the check catches an operator that is inconsistent between two of its own channels, not one that is consistent. Closing that requires an anchor the operator does not control — key-derived ids or a transparency log, both recorded as deferred. - Omission is as effective as substitution, and quieter. Certificate verification is skipped without latching when the trusted set is empty, including when contracts carry an empty
ProvideTlsCertificate(connect/transfer.go:4010-4016), and the identity proof cannot be verified at all when the peer's public key is absent (connect/transfer_encrypt.go:1706-1708). Either way the cipher never becomes usable and traffic runs in plaintext (§2.2) with no user-visible signal. An operator wanting to read a specific user's traffic while the toggle is on does not need to forge anything; it needs to omit a field. - 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:768-779;sdk/device_local.go:2866-2872). 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 does not depend on any key distribution. It does mean that operator blindness to content, on the sealed path, currently rests on the operator distributing keys honestly — which is a policy property with a cryptographic mechanism most of the way built behind it, not yet a property that holds against a hostile operator. Any URnetwork document that describes the sealed session as making operator blindness unconditional is overstating it, and 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:305,340). No protobuf inconnect/protocol/carries a padding field, and framing is a bare 4-byte length prefix (connect/message_framer.go:28-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:387-392). 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:63,232-246), 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:112-215, connect/transport_pt.go:18-45) 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:138-158,1234-1240). 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:461-463; 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 at connect/ip_remote_multi_client_api.go:326-345, released at :410,473).
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:1204-1208), and a stored window identity is replayed against the same destination until it goes stale: windowIdentitiesStaleAfter = 4 * time.Hour (sdk/window_identity_store.go:44-52). 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:3434-3437), which carries no ClientKeySeed (connect/transfer.go:166-183), so ClientKeyManager generates a new keypair (connect/transfer_key.go:96). The persisted window snapshot stores client id, jwt and instance id only — no key seed (sdk/window_identity_store.go:46-52), 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:9149-9157).
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:566-571,1070-1090) — generated with a host octet in 2..254 over the smallest non-conflicting 10.a.b.0/24 (connect/tun.go:323-348). 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:742,1009-1020,1151). 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:418-461, .device_local_key_material, JSON client_key_seed; applied at sdk/device_local_key_material.go:54-70). 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:636-647). 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:639-644).
That key is published, sealed into every contract naming this client as destination (server/controller/connect_controller.go:413-415,468), and readable by anyone: GET /key/ is unauthenticated by design (server/api/api.go:123-124; server/controller/connect_controller.go:826-829). 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:64-67). A top-level client is deactivated after 30 days idle (TopLevelClientIdleExpiration,:2289) and hard-deleted 30 days after that (NetworkClientReapAfterDeactivate,:2269), 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:35-37,188), refreshed by the SDK at half-life — about 12 hours — with jitter (sdk/device_token_manager.go:107-124,165). Refreshing the token does not rotate the client id; the same claim is re-minted (server/model/network_client_model.go:102-125). audit_contract_eventrecords client and provider identity per contract (server/db_migrations.go:234-251).client_reliabilityjoins the keyed IP-block hash directly to a client id, per block: the original four-column primary key (server/db_migrations.go:2083-2106) 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-195), withnetwork_idsurviving as an index payload (:63,631-634). Retention is 30 days (server/model/network_client_reliability_model.go:42,687-721; daily partitions dropped whole).network_client.auth_timeis a durable last-seen retained 30 days (server/model/network_client_model.go:2269,2289); 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:15-29). It is the one entry in the five that is a fingerprinting surface.
9.4 The WireGuard bridge links you across providers
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:1034,1038-1118), written into the config as the interface address (:756-771), and it is not normalised before egress — the source's own FIXME reads "currently the client ipv4 is threaded to the egress providers / this can allow tracing a single client ipv4 across multiple providers" (proxy/wg.go:23-25). It is a private address, not your real IP, and the destination website never sees it. But every provider in your window sees the same source address, so colluding providers can tell that those flows belong to one client — precisely the property the multi-provider window otherwise provides. Combined with the fixed 1.1.1.1 resolver (§3.2) and the absence of a sealed session on that path, the WireGuard bridge is the least private way to use the network. The apps and SDK are the most private.
10. Legal demands
Jurisdiction. BringYour, Inc. is a Delaware corporation (docs/legal/ur.xyz/terms.md:16,25) with a San Francisco mailing address (docs/legal/terms.md:269-271). The ur.io terms set governing law and venue in Harris County, Texas (docs/legal/terms.md:315-321); the ur.xyz terms set Delaware (ur.xyz/terms.md:223-229). 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-260). 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:984,1018) — or, for completed on-chain payments, not at all (server/model/solana_payment_intent_model.go:386-400 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:255-257). 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. The only legal-process pointer anywhere directs a server of process to obtain the Delaware registered agent from the Delaware Division of Corporations (docs/legal/ur.xyz/terms.md:262); [email protected] is scoped to vulnerability reports (docs/legal/vdp.md:42) and [email protected] is the general contractual notice address. URnetwork's own comparison documents already concede this 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.
- A compelled or hostile operator substituting or omitting provider key material. Today the cross-check that would catch it logs and continues. §7.4.
- Silent downgrade of the sealed session — WITH Post Quantum Encryption OFF. In that mode any failure (handshake, identity proof, missing key) falls back to plaintext 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.
- Cross-provider linkability on the WireGuard bridge. One stable tunnel address across every provider in your window. §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.
- What the ingress LB logs. The nginx header configuration is in the tree (
xops/.../connect/ingress.yaml:9) and the connect service rate-limits on a hashed address (server/connect/transport_rate_limit.go:65-80), but nginx's own access-log configuration was not audited. Any end-to-end "we do not retain your address" statement depends on it. - Whether the ungated address- and SNI-bearing log lines in §5 reach durable storage. They are written to stderr; where stderr goes in production, and for how long it is kept, is a deployment question this source review cannot answer. The monitoring tailer does scrape
ip:portpatterns out of log lines and re-emit them into findings (server/monitor/tailer.go:136,328,333), which is evidence that at least some of these lines are read by systems that retain them. - Whether the WireGuard server-side proxy device rewrites the packet source before handing packets to providers. The stable tunnel address and the operator's view of the real public endpoint (
server/proxy/wg_handoff.go:56-67) are both verified; the full path throughserver/proxy/proxy_device.goandOpenProxyDevicehas not been read end to end (review/verified/ARCHITECTURE.md, unverifiable item 3). - 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:1155-1175). Per-browser proxy DNS behaviour under every configuration was not tested. - Actual production verbosity.
BY_LOG_Vdefaults to 0 (server/env.go:41-49), 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:1607-1641), 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:20-30). 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 every device send path uses a window client. The window path's ephemeral keying is verified (§9.1). The device's top-level client carries the durable seed and enables encryption unconditionally (
sdk/device_local_provider.go:90-98), so any send that rides the top-level client — network-peer relationships, companion return paths — is signed with the stable key and carries the stable top-levelclient_id. Those paths were not enumerated exhaustively. Read "egress identifiers are ephemeral" as scoped to the window-client path. - 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:408-415;server/controller/connect_controller.go:474-481). 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. - Whether the ur.io Terms' Texas venue is intentional, given a Delaware entity, a California address, and a Delaware venue in the ur.xyz terms. No document in the tree explains it. This is a question for counsel, not a finding.
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.