# 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](https://support.torproject.org/about-tor/security/attacks-on-onion-routing/):
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 typed `ErrEncryptionRequiredNotEstablished`.
- **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 — `HttpUpgradeUnencrypted` is 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 <client IP>` 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_<clientId>` 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-proof` and sends it as an `EncryptedControl`
  (`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/<client_id>` 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:

1. **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.
2. **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.
3. **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 in
  `connect/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`). Every `jitter` in 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
  `WritePacketsPerSecond` cap (`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/<client_id>` 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_id` is 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 Redis `ckey` (`:2537`).
- `device_id` is **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_event` records client and provider identity per contract
  (`server/db_migrations.go:234-251`). `client_reliability` joins 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`), with
  `network_id` surviving 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_time` is 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 in `user_auth_attempt`, wallet addresses,
  payment rows, a persistent `network_referral` who-invited-whom edge, and
  `audit_provider_event` rows carrying literal country, region and city strings
  against `network_id` and `device_id`, not tied to the 8-hour connection reap
  (`review/verified/PRIVACY-ENFORCEMENT.md` §1.4).
- `User-Agent` is 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`); `security@ur.io` is scoped to vulnerability
reports (`docs/legal/vdp.md:42`) and `notice@ur.io` 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.

1. **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.
2. **A global passive adversary.** Out of scope entirely. §8.3.
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.
4. **A compelled or hostile operator substituting or omitting provider key
   material.** Today the cross-check that would catch it logs and continues. §7.4.
5. **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.
6. **A malicious provider tampering with unencrypted traffic.** Plaintext HTTP is
   passed through unchanged; the provider is in the position of a hostile
   hotspot. §3.
7. **Endpoint compromise.** Malware, a compromised OS, a hostile browser
   extension, or anyone with your unlocked device. Nothing in a network product
   addresses this.
8. **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.
9. **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`.
10. **Sybil providers.** Anyone can provide, with no attestation or stake. A
    single party can run many providers, including the operator.
11. **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.
12. **Cross-provider linkability on the WireGuard bridge.** One stable tunnel
    address across every provider in your window. §9.4.
13. **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/<client_id>` is unauthenticated, so any party can cluster client ids
    that share a key. §9.2.
14. **Traffic the local network sees during the DNS startup window.** Documented
    in the source as an accepted cost. §3.2.
15. **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:port` patterns 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 through `server/proxy/proxy_device.go` and
  `OpenProxyDevice` has 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_V` defaults to 0
  (`server/env.go:41-49`), which suppresses `V(1)`-gated destination logging on the
  client and server — but the only deployment manifest in the tree sets it to `2`
  (`xops/gitops-unused/.../api/deployment.yaml:47-48`), in a directory named
  `gitops-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-level `client_id`. Those paths were not
  enumerated exhaustively. Read "egress identifiers are ephemeral" as scoped to
  the window-client path.
- **Whether `Roles` and `Principal` are ever populated for ordinary consumer
  clients.** They are operator-assigned strings sealed into the signed contract
  bytes and set only for `ProvideMode_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.** `ClientKeySeed` is referenced
  only from `sdk/local_state.go`, `sdk/device_local_key_material.go`,
  `sdk/device_local.go` and `sdk/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: `security@ur.io` and the disclosure policy at
[ur.io/vdp](https://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.
