# 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-09-17**,
when every line-anchored citation in this document was re-resolved against the
trees by identifier. Line numbers drift; the identifiers do not. Where a
finding was established in an earlier code-verification pass, it cites
`review/verified/ARCHITECTURE.md` or `review/verified/PRIVACY-ENFORCEMENT.md`,
which carry the same convention.

That re-resolution was not cosmetic and is worth recording as a caution for
anyone reading an older copy. Between the 2026-08-07 pass and this one
`connect/transfer.go` roughly tripled in length, and almost every `connect`
citation moved — the fail-closed send gate cited at `transfer.go:2796` is at
`:6751` today, and the receive gate cited at `:5844` is at `:14886`. Treat any
line number in a copy of this document that does not carry the date above as
unreliable, and navigate by identifier.

Three labels are used deliberately:

- **Property** — the code enforces it, and an attacker who controls the other
  parties still cannot violate it.
- **Discipline** — the code chooses not to do something it is capable of doing.
  A deployment change or a one-line patch could reverse it.
- **Design intent** — the design says this is the goal; the shipping code does
  not yet enforce it.

## Summary

This section is for readers who will not read the full record. Each sentence
in it is backed by a numbered section below or by the assurance statement
above.

**What the system is.** URnetwork is a privacy network in which members relay
traffic for other members. The path is you → extender → operator → provider →
internet. The trust split is between two relay parties: the operator, which
knows who you are, and the provider, which sees where your traffic goes.

**The question this document exists to answer.** Can an untrusted operator and
an untrusted set of providers still carry a private and anonymous session, so
long as they are not colluding? The honest answer today is **most of the way,
and the gap is precisely locatable**: provider blindness to identity holds
against an arbitrarily malicious provider, unconditionally; operator blindness
to content holds against a passive operator and against any on-path attacker,
but rests on the operator distributing provider identity keys honestly and on
its not choosing your providers adversarially. §1.1 is the trust model that
states this party by party; §7 is the mechanism behind the caveat.

**The two properties, separately scoped.** On a relayed path the provider
never receives your real source IP; no setting or provider build changes that
(§2). The operator cannot read the sealed client-to-provider session, which is
available on the five native apps (Android, iOS, macOS, Windows and Linux)
under the name Post Quantum Encryption (§2.1). The browser extension has no
sealed session — its client device runs inside the operator, which acts as a
protocol translation point, so a seal cannot exist there (§2.4).

**Three trust dependencies, and where each stands.** The seal used to fail open
silently in every mode. **Fixed 2026-08-08/10:** with Post Quantum Encryption
on, the client runs fail-closed and refuses to send or accept plaintext
application data rather than degrading to it (§2.2). Two things remain, and
both are trust in the operator rather than in the cryptography:

1. **Key distribution.** Can the operator substitute a provider key?
   **Substantially harder since 2026-09-17, and no longer deniable.** With Post
   Quantum Encryption on, the client withholds the session cipher until the
   contract-supplied provider key is corroborated against a signed,
   hash-chained, domain-bound registration history the operator has published,
   and a verified disagreement kills the session for that provider (§7.4). An
   operator that substitutes must now *sign* the substitution, leaving a
   permanent attributable record that diverges from what every other reader of
   that client id sees. What it does **not** stop is an operator that is
   malicious from your first contact and consistent about it: it signs one
   coherent chain naming its own key, and a client with no independent view of
   which signer is authoritative cannot tell. Signed is not
   unforgeable-by-the-signer.
2. **Provider selection.** The operator ranks and returns the provider set, and
   the client has no way to check that the providers it was offered are
   independent of each other or of the operator (§5, §6.1). An untrusted
   operator therefore need not *collude with* providers; it can select its own.
3. **And the default.** The fail-closed guarantee is bound to a toggle that
   still ships off in all five native apps (§2.2), so a default-configuration
   client opens no session to the provider at all: every application packet
   crosses the operator unsealed, on the standard path.

`verify/BEFORELAUNCH.md` items 7, 8 and 9 track all three; §2.2, §6 and §7.4
describe the current state.

**What the system does not defend against.** An adversary who observes both
your access network and the providers carrying your traffic can correlate the
two ends by size and timing (§8.1). A global passive observer is out of scope
entirely (§8.3). URnetwork ships no padding, no cover traffic and no mixing;
section 11 is the full list.

**Assurance.** There is no independent audit of the protocol, the connect
engine, or the operator's server code; two 2025 third-party assessments cover
other surfaces: a penetration test of the web application and API, and a
Leviathan Security Group MASA AL2 assessment of the Android app, which
passed.

**Where the detail lives.** Section 1 defines the parties and states the trust
model party by party (§1.1). Section 2 covers the three connection modes and
what each party sees in each. Sections 3 through 5
take each adversary in turn: a malicious provider, a network observer, and
the operator. Sections 6 and 7 cover operator-run providers, collusion, and
provider key substitution. Sections 8 through 12 cover traffic correlation,
device identifiers, legal demands, the list of what the system does not
defend against, and what this document could not establish. Section 13 says
where to report vulnerabilities and corrections.

## 1. The parties

| Party | Run by | Position |
|---|---|---|
| Client | The user | The app or SDK instance; on the browser and proxy paths it runs on the operator's servers instead (§2.4) |
| Ingress LB | The operator | nginx; stamps the observed client address into `X-UR-Forwarded-For` (`xops/gitops-unused/gitops-prod/urnetwork/connect/ingress.yaml:9` — the only ingress manifest in the tree, and its directory is named `gitops-unused`, so it may not describe production; §12) |
| Connect service | The operator | One or two processes on different hosts, joined by an internal exchange connection (`server/connect/resident.go:395`). Not "a relay server" |
| API / control plane | The operator | Auth, provider discovery and ranking, contracts, the public-key lookup (`server/api/api.go`) |
| Provider | Any member | Receives relayed packets and dials the destination from its own connection (`connect/ip.go:7167` `NewRemoteUserNatProvider`; `Receive` at `:8726`, `ReceiveBatch` at `:8486` → `LocalUserNat`, `:731,784`) |
| Extender | Any volunteer | The path's first leg: a TLS forwarding relay on an independent address. Carries the client↔platform session without terminating it, so it sees the connecting IP and cannot decrypt — the inner stream is "the client's own tls to the destination, which the extender never sees inside" (`connect/extender/extender.go:35-44`; `connect/net_extender.go:34-48`) |
| DoH resolvers | Cloudflare, Google, Quad9, OpenDNS | The app path resolves DNS over HTTPS through the tunnel to one of these four (`connect/net_http_doh.go:148-166`) |
| Payment processors | Stripe, Apple, Google, Solana/Circle | Hold real identity for paid accounts; the operator stores the join keys (`server/db_migrations.go:1804`, `:4732`, `:2441`) |

The operator is **BringYour, Inc.**, a Delaware C corporation with a San
Francisco notice address (`docs/legal/terms.md:8-9,265-271` names the entity
and the address; the incorporation is not stated in the published terms).
Section 10 covers what that means for legal process.

A note on vocabulary, because the network's economic layer uses a different
word for the same party. Providers are called **miners** where they are paid
for the bandwidth they carry, and the network is designed to have many
independently operated operators rather than one. Neither changes the
cryptography: the pattern is always one client ↔ one operator ↔ one provider,
and every property below is a property of that triangle. What a plurality of
operators and providers changes is the *plausibility of the non-collusion
assumption* in §1.1, not the mechanism that assumption is protecting.

### 1.1 The trust model

Read this table as: *if this party is arbitrarily malicious and the others are
honest, does the property survive?*

| Property | vs. a malicious **provider** | vs. a malicious **extender** | vs. a malicious **operator** |
|---|---|---|---|
| **Your identity is hidden from the exit** (the provider never learns your address or account) | **Yes — unconditional.** The address is never on the wire; the provider's entry point takes ids only (§2, §3) | **Yes.** An extender sees your address, which your ISP already knows, and learns nothing about the exit. The exception is the provider role, which you turn on by sharing your connection: it identifies itself by its client id to the extenders it measures (§4) | **No.** The operator knows who you are by construction; that is its role (§5) |
| **Your destinations are hidden from the coordinator** (the operator cannot read the session) | n/a — the provider is the party that *sees* destinations | **Yes.** The extender forwards a session it cannot terminate (§4) | **Conditional.** Holds against a passive operator, against downgrade, against an operator that turns malicious later, and against one inconsistent across its own channels. Does **not** hold against an operator malicious and self-consistent from first contact (§7.4) |
| **No plaintext downgrade** (traffic is sealed or does not flow) | **Yes, with the toggle on.** The receive gate refuses stripped plaintext (§2.2) | **Yes, with the toggle on** | **Yes, with the toggle on** — omission becomes denial, not disclosure. **No, with it off**, which is still the shipping default (§2.2) |
| **No browsing record exists to be compelled or leaked** | **Yes** | **Yes** | **Yes — structural.** No destination, host, URL, SNI, port or domain field exists anywhere in the transfer ledger (§5, §10) |
| **Your traffic is not correlatable end to end** | **No** | **No** | **No.** No padding, no cover traffic, no mixing — out of scope by design (§8) |

Three things follow, and they are the honest shape of the answer to the
question in the summary.

**The provider is trusted with nothing, and that is enforced structurally.**
Not by policy, not by the provider running the official build — by the address
never being sent. This is the strongest property in the document.

**The operator is trusted with two things, and both are open.** It distributes
the provider identity keys the seal is checked against (§7), and it chooses
which providers you are offered (§5, §6.1). Neither is verifiable by the client
today.

**Those two are not independent, and the second is the sharper one.** A
document that says "safe unless the operator and providers collude" is
understating the problem while the operator picks the providers: it does not
need to *suborn* anyone if it can *select* whoever it already controls. The
design distinction is substitution versus selection
(`connect/DESIGNNOTES2.md` §3.4). Closing substitution has a cryptographic
answer and one is being built (§7.4). Closing selection needs something the
client can check about the independence of a provider set, and nothing in the
shipping code attempts it.

## 2. The three modes, and what each party sees

This table is canonical and matches `OVERVIEW.md`. Everything else in this
document is the mechanism behind it.

| Mode | Operator sees | Provider sees | Default and availability |
|---|---|---|---|
| **Relayed sealed** | account/source connection, provider association, ciphertext and timing/volume | destination traffic, device/contract id, **not** your real source IP | native apps only, and **opt-in today**: ruled to be the default, still shipping off; see §2.2 |
| **Relayed standard** | account/source connection, provider association, inner destinations and packet bytes | destination traffic, device/contract id, **not** your real source IP | the native default today, since the seal ships off (§2.2), and the browser and proxy paths (§2.4); with the seal on, a provider that cannot be sealed to is skipped rather than served here (§2.2) |
| **Direct** | less relay involvement | **your real source IP** and destination traffic | opt-in, by disabling Strong Anonymization (§2.3) |

Two asymmetric statements follow, and they are not equally strong:

**Provider blindness to identity is a property, and it is unconditional on both
relayed paths.** The provider's inbound entry point takes ids, never an address:
`RemoteUserNatProvider.Receive` is parameterised by a `TransferPath` of three
16-byte ids (`connect/ip.go:8726`, batch form at `:8486`;
`connect/connect.go:50`). No transport protobuf carries a client address, city
or country — a grep over `connect/protocol/*.proto` for
`location|city|country|region` returns nothing
(`review/verified/ARCHITECTURE.md` §1.3). There is no setting that turns this off
on a relayed path, and no provider build can obtain the address it is never sent.

**Operator blindness to content is a setting, not yet a default — over a
mechanism with one defect left.** The mechanism has a name: **Post Quantum
Encryption**, a control in the connect drawer of the Android, iOS, macOS,
Windows and Linux apps, which seals an end-to-end session between your client
and the provider (§2.1). The first condition is that the user finds the
switch, because it still ships off (below). As of 2026-08-10 it is no longer
whether the seal quietly failed to establish: with the toggle on, a connection
that cannot be sealed carries no application traffic at all rather than
carrying it in the clear (§2.2). What remains conditional is whether the
operator, which serves the provider keys the seal is checked against, is
serving them honestly (§7.4). Read that before relying on this property; it
is not visible to the user.

**Default at launch: ruled ON; still shipping OFF as of 2026-09-24.** The
product owner ruled on 2026-08-07 that the sealed session ships enabled, so
that operator blindness would be the default rather than a choice the user has
to make. **That has not landed.** The performance-profile flag is still `false`
in all five apps — `android/.../PerformanceProfileSettings.kt:34,88`,
`apple/app/network/Shared/ViewModels/DeviceManager.swift:395,479`,
`windows/app/src/App/SdkHost.h:192` and `linux/app/src/ConnectDrawer.cpp:746`
— and the SDK starts a device with no profile at all. Without the flag the
window clients keep `DefaultEncryptionSettings`, whose mode is
`EncryptionModeOff` (`connect/transfer_encrypt.go:768-770`;
`sdk/device_local.go:4483`), so a default-configuration client today runs
neither `Required` nor `Opportunistic`: it opens no session to the provider,
and every application packet crosses the operator unsealed
(`verify/BEFORELAUNCH.md` item 7, verdict: fails). Every statement in this
document about fail-closed behavior is scoped to the toggle being on, and a
reader should assume it is off unless they turned it on.

That gap matters more in one direction than it used to. Because the fail-closed
gate is bound to this same toggle, **turning the default on would propagate a
real guarantee to every user** rather than merely widening the reach of a
silent failure — the reverse of what this section warned when the seal could
fail open in every mode. What the default still cannot fix is §7.4, which is a
property of the seal itself: a default-on seal an operator can
machine-in-the-middle protects against a passive operator only.

The **browser extension has no sealed session at all**
(`extension/src/utils/auth-params.ts:33`, `performance_profile: null`). That is
scope, not a default, and the launch ruling does not change it.

Wherever the seal is absent — the browser and proxy endpoints (§2.4), and the
native apps with the seal turned off (§2.2) — the operator terminates the
client's TLS/QUIC and relays plaintext IP packets
(`connect/protocol/ip.proto:9-16`). It *chooses* to parse only the routing
header — `FilteredTransferFrame` contains nothing but the transfer path
(`connect/protocol/transfer.proto:85-87`), and the relay unmarshals exactly
that message and no more (`server/connect/resident.go:3824`) — which is
**discipline**, not a cryptographic barrier.

### 2.1 What the seal is, precisely

A TLS 1.3 session directly between client and provider, carried as ordinary
control frames through the same operator: `EncryptedControl` messages hold the
handshake bytes and ride the normal reliable in-order `Pack`/`Ack` flow, one
session per peer-pair, with the lexicographically lower `ClientId` taking the
TLS-client role (`connect/transfer_encrypt.go:30-60`). Key exchange is the
hybrid **X25519MLKEM768**, with conventional X25519 as fallback
(`connect/transfer_encrypt.go:376-384`); the AEAD is AES-256-GCM over a
32-byte key exported under the RFC 5705 label `urnetwork-connect-aead`, with a
12-byte random nonce per message (`:88-99` for the labels and lengths,
`:283-301` for the construction); identities are Ed25519, generated in-process
by `ClientKeyManager` (`connect/transfer_key.go:87-143`, the only
`ed25519.GenerateKey` call in the three trees, at `:113`) — so "post-quantum"
scopes to the key exchange only. Mutual TLS is required
(`ClientAuth: tls.RequireAnyClientCert`, `connect/transfer_encrypt.go:406`) and
the peer certificate is checked at the sequence layer against the contract's
commitment rather than by the TLS stack; the comment at
`connect/transfer_encrypt.go:386-398` explains that without mTLS the
TLS-server-role side would have no `PeerCertificates` and contract
verification would fail for half of all peer pairs.

### 2.2 Fail-closed when you ask for it, fail-open when you don't

**Updated 2026-08-10. This section previously said the seal fails open silently
with no fail-closed option. That is no longer true, and the change is the most
significant hardening in this document's history** — it closed the downgrade
vector in both directions. What follows is the current behavior, read from
source.

Encryption is still a binary property of `Cipher() != nil`, and a nil cipher
still means the session is not usable: a failed handshake, a failed identity
proof, or a contract that never carried the peer's public key all leave it nil.
The structural reason is worth stating once, because everything else depends on
it — **the AEAD exists before the peer is authenticated and is deliberately
withheld.** `completeHandshake` parks it on `derivedTlsCipher` rather than
exposing it (`connect/transfer_encrypt.go:1719-1726`), and only
`maybeVerifyPendingPeerIdentityProof` promotes an epoch to established, and
only on a verifying proof (`:1928-2050`, promotion at `:1979-1981`). `Cipher()`
reads `establishedEpoch` (`:2625`), so to every caller an unproved peer is
indistinguishable from an incomplete handshake.

One correction to earlier versions of this section: identity-proof failure is
**no longer** merely "left unauthenticated". It now also sets
`identityFailedTerminal`, cancels the epoch, and emits an
`EncryptionEventIdentityFailed` (`connect/transfer_encrypt.go:2005-2030`). The
log line still reads "session left unauthenticated" (`:2027`) and the wording
understates what the code does.

**What changed is what happens next.** There are now three modes
(`connect/transfer_encrypt.go`, `EncryptionMode`):

| Mode | Behavior |
|---|---|
| `EncryptionModeOff` | The zero value. Session layer inert, everything plaintext. |
| `EncryptionModeOpportunistic` | Seals once a session establishes, plaintext until then — or permanently if it never does. The historical behavior. |
| `EncryptionModeRequired` | **Never exposes application data in plaintext to or from a peer for which a session is expected.** |

Under `EncryptionModeRequired` the guarantee is enforced at four points:

- **Send entry gate** (`connect/transfer.go:6751-6800`, in `SendSequence.Pack`).
  An application pack waits for the cipher within the caller's timeout and is
  then *refused, unsent* — never downgraded — with a typed
  `ErrEncryptionRequiredNotEstablished` (`connect/transfer_encrypt.go:505`).
  The gate sits deliberately *before a sequence number is assigned*: the
  client-role handshake rides this same sequence, so holding an
  already-sequenced frame would gap the ordered receive side and strand the
  ClientHello behind the gap, deadlocking the handshake that would clear the
  gate.
- **Send backstop** (`connect/transfer.go:11726-11738`, in
  `writeMaybeWrappedBytes`). A frame that reaches the writer without a cipher
  is refused rather than written, covering the narrow race where a session is
  torn down between enqueue and write.
- **Receive gate** (`connect/transfer.go:14886-14913`). A *plaintext application
  frame* from a peer for which a session is expected is dropped and audited —
  this is the half that matters most, because it closes the downgrade where an
  attacker strips the wrap and the receiver would otherwise accept the
  plaintext. The frame is acked and discarded rather than left unacked, since
  withholding the ack would gap the ordered sequence and wedge both sides.
- **Candidate prefilter** (`connect/ip_remote_multi_client.go:11695-11710`,
  `EncryptionCapabilityPrefilter`, default true at `:196`). A window candidate
  that the platform's out-of-band key API says has never published an identity
  key is failed immediately, since it can never complete the handshake. The
  rejection rule is deliberately narrow (`:13229-13236`): a fetch *error* does
  not reject, so operator unreachability cannot be turned into a provider ban.
  It only accelerates certain failure; it never admits a candidate.

Handshake control frames, acks, and control-plane peers are exempt by design —
the gate covers application payload, not the scaffolding that bootstraps it.

**Which mode you get is the Post Quantum Encryption toggle.** When it is on,
the consumer client runs `EncryptionModeRequired`
(`connect/ip_remote_multi_client.go:13080-13091`, from the profile flag at
`:1311`): a provider that cannot establish a session carries no application
traffic for you at all, rather than carrying it in the clear. The stated cost
is availability, accepted deliberately. Providers run
`EncryptionModeOpportunistic` (`sdk/device_local_provider.go:186`) so that one
provider can serve both sealed and unsealed consumers; that is a responder-side
compatibility choice and does not weaken the initiator's guarantee.

Note that the mode enum's **zero value is `Off`**
(`connect/transfer_encrypt.go:469-504`), so a zero settings struct encrypts
nothing rather than silently half-encrypting. That is a deliberate choice made
when `Encrypt bool` was replaced by the tri-state.

**What this means for the toggle.** With PQE on, the seal no longer fails open:
it fails closed, loudly, on both send and receive. With PQE off, which is how
the apps ship, the client is not even opportunistic: it runs
`EncryptionModeOff` and never starts a session, so all of its application
traffic flows unsealed, and nothing tells the user. **So the honest statement
is conditional, and the default matters**: see the **Default at launch** note
in §2.

The behavior is covered by tests, not just comments —
`TestRequiredEncryptionFailsClosedAgainstPlaintextPeer`,
`TestRequiredGateNonBlockingSendRefusesPreCipher`,
`TestRequiredGateBoundedBudgetRefusesUnsent`,
`TestRequiredSendRefusalTypedErrorAndEvent` and
`TestRequiredContractFreeWithoutKeySourceFailsClosed`.

One stale comment to ignore while auditing: the comment inside
`completeHandshake` at `connect/transfer_encrypt.go:1723-1725` still asserts
unqualified that until the identity proof verifies, "the wrap path observes
cipher == nil and traffic flows in plaintext". That is true only in `Off` and
`Opportunistic` mode; the gates above override it under `Required`. The comment
predates the fix.

**Partly closed since: there is now an identity surface, though not a
per-connection banner.** The SDK exports the set of peers with an established,
**identity-verified** session, together with a canonical display fingerprint —
`sdk/post_quantum_identity.go` defines `PublicIdentityKeyHash` as "THE
canonical display hash for identity keys on every platform: the SHA256 of the
key, encoded as unpadded uppercase base32 (RFC 4648)", with `ProviderIdentity`
at `:24-30` and a view controller at
`sdk/post_quantum_identity_view_controller.go:21`, consumed by both apps
(`PostQuantumIdentityViewModel.kt`, `PostQuantumIdentityStore.swift`). That is
more than a hook: it is the beginning of a user-checkable, operator-independent
comparison channel, since a fingerprint obtained elsewhere can be compared
against the one displayed. What is still missing is a plain per-connection
"this session is sealed" indicator. The remaining device-log signals are
`peer identity proof verified — cipher is now usable`
(`connect/transfer_encrypt.go:2004`) on success, an `Errorf` on failure
(`:2026-2029`), and a `NotifyRequiredSendBlocked` event when the gate refuses.

### 2.3 Direct mode

Turning off **Strong Anonymization** sets `AllowDirect`, whose own field comment
reads: `// setting this to true exposes the real source IP to the provider`
(`sdk/sdk.go:1130-1139`). The flow is forced onto a P2P stream
(`connect/ip_remote_multi_client_probe.go:1207-1209`) over a WebRTC/ICE data
channel (`connect/transport_p2p_webrtc.go:818-825`), and ICE means both endpoints
learn each other's addresses. The default is off — a nil performance profile
yields false (`connect/ip_remote_multi_client.go:841-850`;
`sdk/sdk.go:1130-1139`).

Two forced cases: same-network peers (your own devices) always allow direct
(`connect/ip_remote_multi_client.go:843-850,2656-2660`), and hosted devices force it off
(`sdk/device_local.go:608-615`). Note that direct mode removes the operator
from the *data path only*. Contracts, provider selection and control messages
still go through the platform, so the operator still learns which provider you
used and how many bytes moved.

### 2.4 The path the table does not cover: the browser and the proxy endpoints

On the browser extension, the web app, and the HTTPS/SOCKS5/WireGuard endpoints,
**the operator runs the client.** The extension provisions a server-side proxy
device (`extension/src/utils/auth-params.ts:22-36`, `enable_socks`/`enable_http`)
and the browser is pointed at the operator's proxy endpoint — HTTPS CONNECT by
default (`extension/src/bridge/background.ts:220`;
`extension/src/utils/proxy-manager.ts:264`). The SDK instance that opens
contracts and holds the provider window runs in `server/proxy`, not on the user's
machine.

This is why no sealed session can exist on these platforms, as a matter of
architecture rather than of engineering effort not yet spent. The sealed
session is client↔provider, and its guarantee depends on the client endpoint
living on the user's own device. A browser or a plain proxy client speaks its
own protocol (HTTPS CONNECT, SOCKS5, WireGuard) and cannot run the network's
engine, so the operator runs the client's device remotely and acts as the
protocol translation point between the two. A seal negotiated from that remote
device would start inside the operator — the party the seal exists to blind.
The implementation accepted this tradeoff deliberately: these surfaces exist
for convenience on platforms and protocols that cannot host the full client,
and the docs must not describe them as sealed.

Consequences, stated plainly:

- Provider blindness to identity still holds — the provider still receives only
  ids.
- Operator blindness does not exist on this path in any form. The operator
  terminates the proxy connection, receives the destination host from CONNECT,
  and holds the client's keys.
- The hosted device installs no DNS-upgrade mux (`server/proxy/proxy_device.go:858`,
  `SetUpgradeMuxSettings(nil)`), so the app path's in-tunnel DoH (§3.2) does not
  apply here.

What stands in for encryption on this path is storage discipline: the proxy data
path logs nothing on client-drivable paths, and a regression test pins it —
`proxy/socks5_nolog_test.go`, `TestClientDrivenTrafficNeverLogs`, asserts zero
log lines across malformed, oversize and undialable cases. Two honest caveats:
the test's stated motive is log-amplification DoS rather than privacy, and the
panic-recovery handler does log a client address
(`proxy/socks5_server.go:129-132`). See `review/verified/PRIVACY-ENFORCEMENT.md` §2.2.

## 3. Adversary: a malicious provider

**In scope, and assumed.** Anyone can provide. There is no attestation, stake,
identity check or Sybil resistance on provider participation — a search across
the server, connect and sdk trees for `sybil|kyc|attestation` returns nothing
relevant. A provider runs open-source code on hardware it controls, so assume it
runs a modified build.

**What it sees.** Everything an ISP sees for the traffic it carries: destination
IP and port, packet sizes and timing, and TLS SNI where the client sends it in
the clear. It receives raw IP packets and dials them itself
(`connect/protocol/ip.proto:9-16`; `connect/ip.go` `LocalUserNat`). It also sees
the `client_id` carried as the contract's `SourceId`
(`connect/protocol/transfer.pb.go:1779`;
`server/controller/connect_controller.go:816-830`) — a per-window-slot identifier
that only the operator can resolve to an account, and never the `device_id`,
which has no field on the wire. Its lifetime, and the several things that do
persist across sessions, are in §9.

**What it can do.**

- **Plaintext HTTP: read and modify it.** Port 80 is passed through to the egress
  unchanged by default — `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:4098,4220`), and
  measurement is the operator's, not the provider's self-report, so this is
  bounded — but it is a ranking input, not an integrity control.
- **Correlate flows within its own view.** Per-site affinity pins a site to one
  provider (`connect/ip_remote_multi_client.go:837`), so a single provider
  sees a coherent slice of one client's browsing for the sites it holds.

**What it cannot do without help.** Learn your real IP on a relayed path, learn
your account or email, or attribute a `client_id` to a person. Those require the
operator (§5).

### 3.1 What `ip_security` does and does not constrain

The connect engine's `ip_security` layer runs on the provider's own egress and on
the user's own device, never centrally at the operator — the operator's
`IngressSecurityPolicyGenerator`/`EgressSecurityPolicyGenerator` fields exist but
are never assigned or read (`server/connect/resident.go:263-264`;
`review/verified/PRIVACY-ENFORCEMENT.md` §4.1). It blocks BitTorrent and
file-sharing signatures, opaque non-standard protocols, reputation-listed
destinations and attack-pattern traffic, and it does so on a deliberately thin
basis: 5-tuple plus a payload prefix bounded at 8 packets / 512 bytes.

The reputation tables are generated, not hand-kept. `connect/security` and
`connect/blocker` aggregate public threat-intelligence feeds — abuse.ch Feodo
Tracker botnet C2, Spamhaus DROP and DROPv6, Emerging Threats compromised
IPs, Blocklist.de, CINS Army, TweetFeed, ViriBack, BruteForceBlocker — into
packed range tables (46,789 IPv4 ranges as of the 2026-08-05 snapshot;
`connect/ip_security_cfaa_block.go` header carries the feed list and
attribution). The release pipeline regenerates both tables from the live
feeds (`build/all/run.sh`, the `CONNECT_IP_UPDATE` step), so each release
ships a current snapshot and an up-to-date client blocks the latest listed
address space. The snapshot ages with the installed build; the lists update
per release, not over the air.

The payload-prefix bound is `connect/ip_security_dmca.go:136`, with the
server name cleared out of the
flow key (`:366,386,424`) and counters keyed on `(version, protocol, port)` with
the IP field never enabled (`connect/ip_security.go:371`).

**It is a safety control for honest providers, not a security control against
hostile ones.** It constrains what a provider's connection will *carry outbound*.
It does nothing about what a provider does with traffic it does carry, it runs in
a process the provider owns, and a modified build can disable it. Do not read it
as a limit on a malicious provider.

One reporting detail belongs here because it is a metadata channel. A BitTorrent
signature match returns Incident, which calls `ReportAbuse`
(`connect/ip.go:9014,9110`), transmitting a `PeerAudit` control frame to the
operator carrying the peer's device id and a boolean — no destination, no domain,
no contents (`connect/transfer.go:2971`). The operator has no
handler for that message type today, so nothing is stored on receipt
(`server/controller/connect_controller.go:132,436-437`; a repo-wide grep for
`PeerAudit` in `server` returns zero hits). That is a **discipline** one `case`
statement away from changing. Opaque-encrypted drops are silent with no report
(`connect/ip_security_dmca.go:483-486`).

### 3.2 DNS

On the app and SDK path, plain DNS on UDP/TCP 53 is intercepted and resolved over
DoH **through the tunnel** (`connect/ip_mux_upgrade.go:120-148`, installed by
default at `sdk/device_local.go:7105`), to Cloudflare, Google, Quad9 or OpenDNS
(`connect/net_http_doh.go:150-155`). A provider therefore sees an HTTPS
connection to a public resolver, not the query. Two honest limits:

- **A startup DNS-leak window is documented in the source.** While the tunnel DoH
  is still establishing, a query is raced against a resolver over the **local
  host** egress, "at the cost of a brief DNS leak during startup"
  (`connect/ip_mux_upgrade.go:120-126,78-92`). Your local network and ISP can see
  those queries.
- **The WireGuard endpoint hands the client a fixed public resolver**, `DNS = 1.1.1.1`
  (`server/model/network_client_proxy_model.go:774`), and port 53 is passed
  uninspected by the security policy (`connect/ip_security_cfaa.go:113`). On that
  path DNS queries traverse the provider as plain DNS, and the provider can read
  and answer them.

The four DoH resolvers are third parties that see queries arriving from a
provider's egress address, unlinked to your account. That is a real dependency
and it is not configurable to zero: DNS has to be answered by somebody.

## 4. Adversary: a network observer

Four positions, from weakest to strongest.

**Your local network and ISP.** They see that you connect to
`connect.bringyour.com` over TLS/QUIC, or to an extender, plus sizes and timings.
They do not see destinations inside the tunnel, except in the documented startup
DNS window above. Where the platform is blocked, extenders present as ordinary
services on believable ports and clients rotate personas with randomised
fragmentation (`connect/net_extender.go:123-133`; built at `connect/net_extender_network.go:915`), and a DNS-shaped
transport exists for networks where only DNS escapes
(`connect/transport_pt.go:37,73`). These are mechanisms for **crossing local
and regional firewalls**, ranked below the direct transports
(`connect/transport.go:29-33`).
They are not traffic-analysis defences and should never be described as such.

**An extender operator.** Anyone can run one, the app accepts a manually entered
IP, and by default no signature is required
(`connect/extender/extender.go:35-44`). An extender is the TCP peer, so it sees
the connecting user's IP, and it cannot decrypt what it forwards, because the
TLS session runs through it to the platform rather than terminating on it
(`connect/net_extender.go:34-48`). The extender leg therefore puts a first-hop
observer of your address in the path who is not the operator. That is the trade
the design makes to get you through a block, and it is worth being exact about
its size: the extender leg adds no encryption and no anonymity — it learns what
your ISP already knows and nothing more, with the one exception below. In the
shipping client the direct transports are raced first and the extender dialers
expand when those fail (`connect/net_http.go:1686-1709`); a client configured
with custom extenders uses them as its only route
(`connect/net_http.go:1686-1709`).

**The exception is the provider role.** Every client probes extenders to rank
them by round trip. A device attests what it measured only while it provides:
attestation starts when you switch providing on and stops when you switch it
off, even partway through a probe pass, and a device that never provides
probes anonymously (`sdk/device_local_provider.go:1155-1167`;
`connect/net_extender_network.go:1240`). While it provides, each claim carries
its client id and is signed with its client key (`connect/DESIGNNOTES4.md` §1,
§3). The extender learns that the provider with that id, and so that public
key (§9.2), measured it from that address; the operator receives the provider
id, the extender, the round trip and the time. Extenders measure each other
the same way, a target co-signs each round trip it accepts, and the operator
keeps the pings for a day to derive locations (`connect/GEOMAP.md` §2). This
is operational data about public infrastructure, under a key that is already
durable and public in the provider role.

**An observer of one end.** Watching only the client side yields "this user is
connected to URnetwork" and a byte/timing profile. Watching only a provider's
egress yields destinations and a byte/timing profile with no identity attached.

**An observer of both ends.** See §8. This is the adversary URnetwork does not
defeat.

## 5. Adversary: the operator

The operator authenticates clients, selects and ranks providers, authors
contracts, distributes public keys, and carries the packets on the relayed paths.
Assume for this section that it is hostile or compelled.

**What it sees, without any additional effort:** your account; your address at
the moment you connect; a city/region/country derived from that address by a
**local** database lookup — `ipDb` in `server/ip.go` opens the bundled MaxMind
GeoLite2-City database (`mmdb/geolite2.mmdb`) from disk and `ip.go` makes no
network call of any kind, so an address is never handed to a geolocation
service — persisted per connection (`SetConnectionLocation` in
`server/model/network_client_location_model.go`); which providers served you;
byte counts; and on the standard path, the destination addresses and packet
bytes inside the tunnel — though those are normally already inside the
destination's own HTTPS.

**What it stores.** Connection and auth records keep a keyed one-way hash of the
surrounding /29 (IPv4) or /56 (IPv6) block rather than the address
(`server/ip.go:46-72`). Precision that matters to an auditor: it is a single
process-wide **pepper** from the vault, memoized once, with no per-row salt and
no rotation mechanism anywhere in the repo (`server/ip.go:42-43`); the IPv4
keyspace is 2^29 blocks, so anyone holding the pepper can invert it by brute
force. The pepper's secrecy is the whole security property. The `/verify`
subsystem uses /48 for IPv6, not /56 (`server/ip.go:74-89`;
`server/model/verify_model.go:113-116`). The source port is stored in cleartext beside
the hash (`server/db_migrations.go:2044`). The account-creation audit row records
the peppered hash and port, never the raw `ip:port`
(`server/model/network_model.go:1157`), and audit rows are removed after 180
days (`server/model/audit_model.go:1018,1052`, scheduled at
`server/taskworker/taskworker.go:85-88`).

**What does not exist to be stored.** No destination, host, URL, SNI, port or
domain appears anywhere in the transfer ledger — a grep of the full migration file
for those terms returns zero hits (`review/verified/PRIVACY-ENFORCEMENT.md` §1.2).
HTTP logging passes an allowlist of exactly five headers
(`server/http_log.go:13-29`), pinned by test. No Prometheus metric carries an IP,
host, destination, port or client id; of 39 declared metrics only three are
labelled and every label is a bounded enum (`review/verified/PRIVACY-ENFORCEMENT.md` §2.5).
Support-tab log uploads are drained to `io.Discard`
(`server/controller/log_file_controller.go:76`); only metadata persists.

### 5.1 How long any of it is kept, and where it goes

Retention is enforced by scheduled sweeps in `server/taskworker`, not by
policy. The windows below are read from the constants those sweeps call, and
they are shorter than the privacy policy — which states no retention period at
all — would lead a reader to expect.

| Data | Kept for | Enforced at |
|---|---|---|
| Connection rows, and the per-connection city/region/country, latency and speed that hang off them | **8 hours** | `taskworker/work/network_client_work.go:91`; the delete cascades `network_client_location` in the same statement (`model/network_client_model.go:2580`) |
| A top-level client idle (no auth, no connect) → marked inactive | **30 days** | `TopLevelClientIdleExpiration` (`model/network_client_model.go:2576`) |
| An inactive client → hard delete, with cascades | **+30 days** | `NetworkClientReapAfterDeactivate` (`:2556`) |
| So an unused device, end to end | **~60 days** | the two chained |
| Completed contracts | **7 days** | `taskworker/work/subscription_work.go:21-43` |
| Audit rows | **180 days** | `model/audit_model.go:1018,1052` |

Two notes an auditor should have. The idle window was **tightened from 90 days
to 30 on 2026-07-18**; the constant carries its own rationale, and a stale
comment at `taskworker/work/network_client_work.go:83-91` still says 90, which is
the likeliest source of that figure if you meet it elsewhere. And
`RemoveLocationLookupResults` is scheduled every cycle but is a **no-op** —
its body and its model call are both commented out and no such table exists.
It is a vestigial stub, not unswept data; per-connection location is deleted
with the connection row above.

**Geolocation never leaves the machine.** The city/region/country lookup reads
the bundled MaxMind GeoLite2-City database, `mmdb/geolite2.mmdb`, from local
disk (`ipDb` in `server/ip.go`), and `ip.go` contains no HTTP client and makes
no network call. The operator refreshes that file with MaxMind's `geoipupdate`
client, a download that sends MaxMind nothing about any user. This product
includes GeoLite2 data created by MaxMind, available from
[https://www.maxmind.com](https://www.maxmind.com). GeoLite2 incorporates
[GeoNames](https://www.geonames.org) data, available under
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). No address is sent
to a geolocation service, and no personal information is shared with any third
party. The only third parties in this document's parties table hold data the
user hands them directly by choosing that path: the DoH resolvers a user's own
DNS queries reach through the tunnel, and the payment processors a user signs
up with to be billed.

**The ingress LB does not log client addresses, and that is checkable.** The
load balancer's nginx configuration is generated by **warp**, which is
open source under Apache 2.0 (`warp/LICENSE`), so this is source anyone can
read rather than a deployment assertion. Two layers:

- The access log uses a purpose-built format that omits the address —
  `log_format noclientaddr` carries time, connection, host, request, status,
  bytes, timings, upstream and user-agent, and the config comments name what is
  deliberately absent: `$remote_addr`, and also `$http_x_forwarded_for` and
  `$http_referer`, "both [of which] can carry a client address forwarded from
  elsewhere" (`warp/warpctl/config.go:1243-1254`).
- The error log cannot be formatted, so nginx's own `, client: <addr>` field is
  removed downstream instead: the lb runs nginx with its stderr wrapped in a
  `ClientAddrScrubber` (`warp/nginx.go:98-103`) that strips that field by regex
  and then replaces **every** remaining IPv4 or IPv6 literal anywhere in the
  entry with `[scrubbed]`, keeping the port (`warp/nginx.go:111,116,176-232`).
  Nothing is exempted by range, so no rule about which addresses belong to users
  can be wrong.

A regression test pins it: `TestNginxLogsOmitClientAddr`
(`warp/warpctl/config_test.go:1208`) fails if any `access_log` names no format,
because nginx's built-in "combined" fallback leads with `$remote_addr`.

**The service containers are scrubbed at the descriptor, not at the call
site.** warp runs service containers with `--log-driver=journald`
(`warp/warpctl/run.go:200`) and does not filter their output, so for a time any
line a service wrote with an address in it reached journald as written. The
call sites were real and several: `server/http.go:413,453` construct
`http.Server` with no `ErrorLog`, so the Go standard library writes
`http: TLS handshake error from <client IP>` on every half-open connection —
the hole `proxy/http.go:199,273` closes with `ErrorLog: discardLog`;
client-supplied SNI is logged at ERROR (`server/tls.go:111`); a raw caller
address at `server/proxy/proxy_device.go:858`; WireGuard peer identity at
`server/proxy/server.go:699`.

Those are no longer the boundary, because the boundary is no longer the call
site. Each service replaces its own stdout and stderr **file descriptors** with
pipes at the first statement of `main` and scrubs everything written to them
(`server.ScrubProcessLogs`, `server/scrub.go:62`, called from all eight
services — `server/cli/{api,connect,alt,proxy,taskworker,gossip,monitor,mcp}/main.go`).
Operating on the descriptor rather than the writer means it covers `log`, glog,
net/http's default `ErrorLog`, any dependency, and any call site added later,
without anyone having to find them. It uses the same `warp.ScrubAddrs`
(`warp/nginx.go:207`) the lb uses for nginx, so there is one implementation of
the scrubber rather than two that can drift.

Two deliberate limits, both of which an auditor should weigh rather than take
on trust:

- **Crash dumps pass through unscrubbed.** A `panic:`, `fatal error:`,
  `signal SIG` or `goroutine ` line latches the scrubber into passthrough for
  the remainder of that process's life
  (`scrubPassthroughMarkers`, `server/scrub.go:48`). A dump is rare and is the
  most valuable thing in the log when it happens, so it is kept whole —
  accepting that a stack frame or register can carry an address. The latch
  writes a visible notice when it trips, so a process that stopped scrubbing is
  not something an operator has to infer.
- **It scrubs what parses as an address.** Version strings and colon-separated
  identifiers that parse are redacted too. The most visible consequence is
  that `server/monitor/tailer.go`, which scrapes `ip:port` out of log lines,
  now sees `[scrubbed]:443`.

So "a service does not write your address to its logs" is now a property of the
process rather than of its call sites. What this does **not** establish is what
journald then does with what it receives; §12 records that as open. Full detail
on the original call sites: `review/verified/PRIVACY-ENFORCEMENT.md` §2.4.

**What the operator controls that is easy to overlook.** Provider discovery is
entirely operator-side: `FindProviders2` is the only live endpoint
(`server/api/api.go:140`), the platform scores and ranks candidates
(`server/model/network_client_location_model.go:5056`), and the
client takes the ranked set it is given. The only consumer-side control is a
location block list (`server/api/api.go:149`). A client has no way to verify
that the providers it was offered are independent of each other or of the
operator.

## 6. Operator-run providers, and operator–provider collusion

### 6.1 Can the operator run providers?

**Yes, and nothing in the system marks or prevents it.** Providing is open to
anyone with no attestation or stake, and there is no first-party, official or
operator-owned provider concept anywhere in the code — searches across `server`,
`connect` and `sdk` for such a notion return nothing. An operator-run provider
would be indistinguishable from a member's.

Combined with §5's point that the operator ranks and returns the provider set,
this means the operator can, in principle, place its own providers in a user's
window. There is no client-side diversity check, no attestation of independence,
and no published measurement of the fleet by an outside party. The counterweights
that exist are real but partial: the window holds several providers at once — a
quality window of 2–6 (hard cap 12) and a speed window of 1–2 (hard cap 4), both
live in the default profile (`connect/ip_remote_multi_client.go:160-189`) — and
providers are removed on unhealthy stats, blackhole detection, ping failure and
per-channel lifetime rotation (`:37-54`, `:610-616`). Those bound how much any one
provider sees. They do not bound a set of providers chosen by one party.

### 6.2 What operator and provider reconstruct together, per mode

Assume the operator and one or more providers in your window share what they
each hold.

| | Relayed standard | Relayed sealed | Direct |
|---|---|---|---|
| Your identity (account, email/wallet/payment) | operator | operator | operator |
| Your real IP at connect time | operator | operator | operator, **and** the provider directly |
| Destinations you visited | operator (inner packets) **and** provider | **provider only** | provider |
| Link between the two | trivial — one party already holds both | the contract: `audit_contract_event` pairs client and provider identity per contract (`server/db_migrations.go:325-347`) | trivial |
| Result | full browsing attribution for whatever the colluding providers carried | full browsing attribution for whatever the colluding providers carried | full browsing attribution, plus the provider knows your address unaided |

**The honest conclusion: the sealed session does not defend against
operator–provider collusion.** It removes the operator's *independent* ability to
read your traffic; it does not stop a provider that already sees your
destinations from handing them to an operator that already knows who you are.
The contract record that joins the two is not a leak — it is the accounting the
network runs on.

What the seal does buy against this adversary is a scope limit: with it on, the
operator's own view contains no destinations, so collusion requires the
cooperation of the specific providers that carried the specific traffic, at the
time it was carried, rather than a query against records the operator holds
alone. That is a meaningful difference in effort and in what compelled
disclosure can reach retroactively (§10). It is not immunity, and no arrangement
of three parties where one selects the others provides immunity.

**The design's structural answer, which is not deployed.** The wire protocol and
the client support chaining additional provider intermediaries — `MaxMultihopLength = 8`
(`connect/connect.go:15-18`), `IntermediaryIds` on `CreateContract`
(`connect/protocol/transfer.proto:547-561`), client acceptance at
`connect/ip_remote_multi_client_api.go:661-662`, server plumbing at
`server/controller/connect_controller.go:915`. The live discovery endpoint never
populates the field — `FindProvidersProvider` is constructed at exactly two sites
and neither sets it (`server/model/network_client_location_model.go:5044,5093`; the field is declared at `:3693` and assigned at neither)
— so the shipping network assigns chains of length one. Provider chaining is a
capability of the protocol, not a property of the deployment, and quoting the
number 8 as a hop count would be misleading.

## 7. Provider identity keys, and whether the operator can substitute one

This is the section most worth attacking, because the answer is uncomfortable and
the source says so itself.

### 7.1 Issuance and binding

Every `connect.Client` — a provider client, and each of the user's window clients
— holds an Ed25519 keypair generated in-process by `ClientKeyManager`
(`connect/transfer_key.go:87-143`, the only `ed25519.GenerateKey` call in the
three trees). It is long-lived where a seed is persisted and reloaded, which is
the provider client, and fresh per process where none is, which is the window
clients; §9 covers the difference and why it matters. The private half never
leaves the process; a wrong-size seed is a hard construction error rather than a
silent fresh identity (`:84-94`). The public half
is published to the platform in a `ClientKey` control message
(`connect/protocol/transfer.proto:684-700`) and stored server-side in Redis at
`ckey_<clientId>` with **no expiry** — Redis is the source of truth, there is no
SQL table (`server/model/network_client_key_model.go:19-65`).

Binding is therefore `client_id → public key`, and the operator issues the
`client_id` and holds the mapping. There is no external anchor: ids are not
derived from keys, and there is no transparency log or DHT. Both alternatives are
recorded in the design as considered and deferred
(`connect/DESIGNNOTES.md` §3.7, "Deferred alternatives").

### 7.2 Rotation and revocation

Republishing overwrites — `SetClientKey`'s own comment reads "keyed on
`client_id` (rotation overwrites)" (`server/controller/connect_controller.go:1193-1199`).
The entry is deleted when the client id is reaped
(`network_client_key_model.go:67-78`, called from `RemoveDisconnectedNetworkClients`).
There is **no scheduled rotation, no expiry, and no revocation list.** A provider
client persists its key material and reloads it, so its identity is stable across
restarts (`sdk/device_local.go:3592-3597`; `sdk/local_state.go:788`)
— and, on Android, deliberately stable across an automatic logout wipe as well
(§9.2). A client with no persisted seed generates a fresh identity each process
start, which is what the user's window clients do. Mid-session key changes are
refused: `SetPeerClientPublicKey` is first-write-wins, and a differing later key
is logged and ignored (`connect/transfer_encrypt.go:2051-2096`).

### 7.3 Verification, and the two defences

- **Defence 1, signed cert binding.** The provider signs its ephemeral TLS cert
  chain with its Ed25519 key and publishes both
  (`connect/protocol/transfer.proto:494-522`). The platform attaches the chain,
  the signature and the provider's public key to every contract naming that
  provider (`:340-368`, `:388-406`). The client admits the chain only if the
  signature verifies under the provider's public key, and then checks the cert
  presented in the handshake against the admitted chain
  (`connect/transfer.go:9560-9583`, checked at `:11928`).
- **Defence 2, in-handshake identity proof.** After the TLS handshake each side
  signs the RFC 5705 exporter output under label
  `urnetwork-sequence-identity-proof` and sends it as an `EncryptedControl`
  (`connect/protocol/transfer.proto:615-660,701-712`). The AEAD is withheld from the wrap
  path until the peer's proof verifies (`connect/transfer_encrypt.go:1719-1726`,
  `:1719-1756`), so a MITM who terminates TLS on one leg and re-handshakes on the
  other produces mismatched exporters and cannot forge the signature.

Both defences verify against `peerClientPublicKey` — and that value is taken from
the platform-authored contract (`connect/transfer.go:9560-9583`;
`connect/transfer_encrypt.go:2051-2096`).

### 7.4 Can the operator substitute a provider key? Signed, attributable, and much harder — but not impossible.

**Updated 2026-09-17. This section previously read "Today: yes." That is no
longer the whole answer**, and the change is the second-most significant
hardening in this document's history after the fail-closed fix.

**The attack, unchanged in shape.** An operator that substitutes the
certificate, the certificate signature and `destination_client_public_key` in
lockstep defeats both defences in §7.3, because both verify against a key the
same contract supplied. The design notes state it in the repository's own words
(`connect/DESIGNNOTES.md` §3.7).

**What now stands in the way.** With Post Quantum Encryption on
(`EncryptionModeRequired`), the client withholds the session cipher until the
contract-supplied identity key is corroborated against a **signed registration
history** the operator has published, and a verified disagreement is terminal
for that provider — no traffic flows to it and it is dropped from the window.

Each registration (`connect/transfer_key_history.go:200-213`) carries the
deployment domain, a monotonic generation, the identity key, a link to its
predecessor's content hash, and a secp256k1 signature by the operator's root
signer. The client checks canonical encoding, signature recovery, chain
contiguity from generation 1, domain and signer authority, and forward-only
movement against a pinned head (`VerifyClientKeyHistory`, `:422`). The gate
lives in `Cipher()` (`connect/transfer_encrypt.go:2699-2716`) and the decision
table in `connect/transfer_key_history_session.go:157`. Server side:
`server/model/st_client_key_history.go` stores the signed chain,
`server/controller/st_client_key_history_public.go:77` serves it, routed at
`server/api/api.go:226`.

**So the operator can no longer be inconsistent and deniable.** To substitute,
it must commit in signed form to a chain, and that artifact is permanent,
attributable, and divergent from what every other reader of that client id sees
— including the independent validators already auditing this log. The older
unsigned cross-check against `GET /key/<client_id>` still runs alongside and is
still advisory, logging `CONTRACT vs FETCHED ... MISMATCH ... (today: log only)`
at `connect/transfer_encrypt.go:2131-2134`.

Four precisions an auditor should hold onto.

1. **Signed is not unforgeable-by-the-signer.** An operator malicious from your
   first contact and self-consistent about it signs one coherent chain naming
   its own key. Verifying the chain checks the chain, not the authority behind
   it; the protocol says so itself — *"The caller must separately authenticate
   the expected signer in the operator's pinned chain version"*
   (`sn/protocol/client_key_history.go:166-167`). The client answers with
   build-pinned signers plus trust-on-first-use
   (`connect/transfer_key_history.go:491`), which catches an operator that
   turns malicious, targets a subset, or forks — and not one that was never
   honest. **Closing that requires the plurality read across independently
   operated operators, which is deferred** (`connect/DESIGNNOTES3.md` §10).
2. **Omission is still the cheaper move, and the ratchet is what bounds it.**
   Certificate verification is skipped without latching when the trusted set is
   empty (`connect/transfer.go:11915-11926`), and the identity proof cannot be
   verified at all when the peer's key is absent
   (`connect/transfer_encrypt.go:1947-1950`). Withholding the signed history is
   the same move one level up. It is bounded by a tier ratchet: a peer once
   verified against signed evidence is never again accepted without it, and
   once an operator has served signed evidence to an install, a peer with none
   is refused (`connect/transfer_key_history_session.go:168-185`, persisted at
   `sdk/peer_client_key_pin_store.go`). A **fetch error** is deliberately not
   treated as a rejection — otherwise one API outage would exclude every
   provider for every user at once — so an operator that breaks its own
   endpoint degrades clients to the contract key. That is an accepted
   availability trade and a real residual.
3. **All of it is scoped to the toggle.** Enforcement runs only under
   `EncryptionModeRequired` (`connect/transfer_key_history_session.go:91`),
   because refusing under Opportunistic degrades to plaintext, which is what a
   substituting operator wanted. With the toggle still shipping off (§2.2), a
   default-configuration client gets none of this.
4. **Contract authenticity is rooted in the operator too.** A provider verifies
   the contract HMAC using a provide secret key that the provider generates and
   publishes to the platform (`server/controller/connect_controller.go:727,840`;
   `sdk/device_local.go:3592-3597`). The operator holds a copy, which is what
   lets it author contracts at all. That is expected of an accounting
   authority, but it means "the contract is genuine" is not an
   operator-independent statement.

**What this does and does not undermine.** It does not touch provider blindness
to identity, which depends on no key distribution. Operator blindness to
content on the sealed path is no longer a bare policy property: substitution
now costs the operator a signed, permanent, third-party-detectable artifact.
It is still not a property that holds against a determined operator that was
never honest, and any URnetwork document describing the sealed session as
making operator blindness unconditional is overstating it. This document
supersedes such wording.

## 8. Timing and traffic correlation, and the global passive observer

### 8.1 There are no traffic-analysis defences. None.

URnetwork ships **no cover traffic, no padding, no mixing, no batching delay and
no traffic shaping** on user data. This was checked exhaustively across
`connect`, `sdk` and `server`:

- No chaff, decoy, dummy or cover-traffic generator exists anywhere.
- The AEAD is length-preserving by construction: ciphertext length is nonce +
  plaintext + tag (`connect/transfer_encrypt.go:303-336`). No protobuf in
  `connect/protocol/` carries a padding field, and framing is a bare 4-byte
  length prefix (`connect/message_framer.go:29-31`).
- Send-side coalescing is explicitly zero-delay: *"There is no batching wait: the
  sequence only takes a second Pack when it is already queued"*
  (`connect/transfer.go:2971`). 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:68,359-361`), which exists
  to be resolver-friendly and applies only to the least-preferred transport mode.
  Its idle "pump" queries are a distinguishable length from data-bearing ones, so
  it conceals neither size nor rate.

**Therefore: an adversary who observes traffic entering your device and traffic
leaving the providers that carry it can correlate the two by size and timing.
URnetwork does not defend against that adversary.** This is the same statement
Tor makes about end-to-end correlation, and it applies here with less margin,
because URnetwork's design goal is low latency — which is exactly the property
that makes correlation easier. A latency-bounded four-leg path is a deliberate
trade against a mixnet's added delay, and this is the side of that trade the user
pays. Counting the extender leg does not change it: that hop forwards the
encrypted session on to the operator without terminating it, so it adds no
layer for an observer to strip and no delay for one to lose you in.

Two things are sometimes mistaken for defences and are not. Extenders and the
shaped transports (`connect/net_resilient.go:114-215`,
`connect/transport_pt.go:37,73`) target DPI-based blocking, and the resilient
layer disables itself once the stream is established (`net_resilient.go:102,146-161`)
— it never touches data-phase records. TLS session-ticket caches are deliberately
not shared between egress paths so a server cannot link them through a redeemed
ticket (`connect/net_tls.go:38-42,99-103`); that is ticket unlinkability, not
traffic analysis.

Nothing in the source acknowledges traffic correlation as a limitation: a search
for `traffic analysis`, `timing correlation`, `traffic correlation` and `global
adversary` across all three trees returns zero hits. The in-repo threat model
(`connect/DESIGNNOTES.md` §3.7) is entirely about the operator MITM. This document
is the first place the gap is written down.

### 8.2 The multi-provider window is not an anti-correlation mechanism

Traffic normally exits through several providers at once — commonly three to
eight across the two windows — and per-site affinity keeps a given site on one
provider (`connect/ip_remote_multi_client.go:160-189,837`). That genuinely
limits how much any one exit sees. But the window's design rationale in the source
is reliability throughout — bad-destination mitigation, health-weighted resizing,
blackhole detection (`:27-52`) — and the one comment reading "lower affinity is
more private" sits on `ClientAffinityTimeout`, a knob that is **commented out**
(`:587-591`, and again in the defaults at `:220`), with `DestinationAffinity`
shipping true for reliability reasons (`:270`). Plural exit identity is a real
benefit and it is fair to claim; claiming it as a designed defence against
correlation is not supported by the code.

### 8.3 The global passive observer: out of scope

A global passive adversary — one able to observe a large fraction of internet
links simultaneously — **is out of scope, and URnetwork provides no defence
against it.** With no padding and no cover traffic, such an adversary correlates
flows across the relay and the providers directly. This is the same position Tor
takes, and unlike Tor, URnetwork does not even add per-hop delay that would raise
the cost.

What is genuinely different, and is worth stating without inflating it: the
egress set is a churning population of independent residential connections rather
than one operator's published address ranges, so an adversary must observe a
wider and shifting set of endpoints to cover a single user, and the endpoints
cannot be enumerated from a server list. That raises cost. It does not change the
outcome for an adversary who can already see both ends.

## 9. Device identifiers and linkability

### 9.1 What a provider can observe about you

**The contract's `SourceId` is a per-window-slot client id, not your device.**
`StoredContract.SourceId` is the authenticated caller's `client_id`
(`server/controller/connect_controller.go:816-830`; parsed by the provider at
`connect/transfer.go:6088-6106`). The generator mints a fresh client id, jwt and
instance id for each window entry and removes it on teardown — the source says so
directly: *"The api generator mints an ephemeral platform client id (with a fresh
instance id) for every window entry and removes it on teardown"*
(`connect/ip_remote_multi_client_identity.go:10-15`; minted through
`AuthNetworkClient` at `connect/ip_remote_multi_client_api.go:691-727`,
released through `RemoveNetworkClient` at `:802-832`). Window membership churns
on top of that: providers leave on unhealthy stats, blackhole detection, ping
failure and per-channel lifetime rotation
(`connect/ip_remote_multi_client.go:37-54`), so a slot id is short-lived by
construction rather than by policy.

**Your traffic is spread across several providers at once, so no one exit sees
the client.** The default profile runs a quality window of six and a speed
window of one to two (`connect/ip_remote_multi_client.go:160-189`). Each
provider holds a slice of your activity under its own slot id, and per-site
affinity keeps a given site on one provider rather than smearing it across all
of them (`:837`). This genuinely bounds what a single honest-but-curious exit
can reconstruct. It is **not** an anti-correlation defence and §8.2 says why.

**The window ids and your provider-role id are separate identity spaces, so a
provider cannot reverse a slot id back to you.** If you also share your
connection, your *provider* client is the top-level client holding the durable,
persisted key seed (§9.2). The window clients are different clients: built from
a fresh `DefaultClientSettingsWithBufferSize` (`sdk/device_local.go:4349`) that
carries no `ClientKeySeed`, so each generates its own keypair per process, and
your own client id is explicitly excluded from your own candidate set
(`sdk/device_local.go:4338`). A provider that holds a slot id and looks it up
via the unauthenticated `GET /key/<client_id>` therefore gets a key that is
fresh for that process and links to nothing durable — not to your provider
identity, not across restarts, not across slots. The durable publicly
resolvable key in §9.2 belongs to the provider role, and nothing on the egress
path exposes it.

**The device's own client id does not reach a provider on the egress path, and
the trace is short enough to check.** `DeviceLocal.sendPacket` does set
`source := connect.SourceId(self.clientId)` — the device's top-level id — and
hands it to the multi-client (`sdk/device_local.go:4870`, batch form at `:4968`). That value is
used only for *local* bookkeeping: keying egress fragment reassembly and the
`provideMode` relationship check (`connect/ip_remote_multi_client.go:5876-5906`).
The wire send is made by the per-slot window client —
`self.client.SendMultiHopWithTimeoutDetailed(frame, self.args.Destination, …)`
(`connect/ip_remote_multi_client.go:14941-14948`), on a `Client` the generator
minted for that slot (`:13094`) — and the device source id is not among its
arguments. A grep for `source` across that send region returns nothing.

**The one identity that is durable is the provider role, and it points the
other way.** A device that shares its connection runs a second client built at
`sdk/device_local_provider.go:198` with `ProviderStreamPolicy = true` (`:191`)
and the persisted key seed. Network-peer relationships and provider-side return
traffic ride *that* client, so they carry the stable top-level `client_id` and
the durable key. That is the identity a device **serves under**, not the
identity it browses under: it is exposed to the consumers a provider serves,
which §9.2 documents in full, and it is never handed to the exits that carry a
consumer's own traffic. The two identity spaces are the ones described above,
and they do not cross.

**But those identities are deliberately reused for up to four hours, on every
path — not only hosted ones.** The device installs its own local identity store
unless it is hosted (`sdk/device_local.go:824-830`), and a stored window
identity is replayed against the same destination until it goes stale:
`windowIdentitiesStaleAfter = 4 * time.Hour`
(`sdk/window_identity_store.go:42-45`). The purpose is to keep a provider's NAT
flows alive across a restart (`connect/ip_remote_multi_client_identity.go:17-23`).
The cost is that a provider can see the same `SourceId` reappear from you across
app restarts within that window. Say four hours, not "ephemeral".

**The client Ed25519 identity key is fresh per window client, on the egress
path.** Window client settings are built from a fresh
`DefaultClientSettingsWithBufferSize` (`sdk/device_local.go:7105`), which
carries no `ClientKeySeed` (`connect/transfer.go:1547-1558`), so `ClientKeyManager`
generates a new keypair (`connect/transfer_key.go:113`). The persisted window
snapshot stores client id, jwt and instance id only — no key seed
(`sdk/window_identity_store.go:42-45`), so a restored identity republishes a new
key. With Post Quantum Encryption off, no identity proof is presented at all
(`connect/ip_remote_multi_client.go:13080-13091`).

**The tunnel source address is per session and low-entropy.** The SDK assigns its
TUN a random RFC1918 address — "a random 10.x.y.h (RFC1918, DHCP-shaped)"
(`sdk/device_local.go:7105`) — generated with a host octet in 2..254
over the smallest non-conflicting `10.a.b.0/24` (`connect/tun.go:517-523`). It is
computed in the constructor and never persisted, so it changes each session. The
provider **does** see it: it is the source field of every tunnelled packet and the
provider keys NAT state on it (`connect/ip.go:731,784`). Roughly 253
values is a weak per-session fingerprint, not a cross-session identifier.

### 9.2 The provider-role identity key is durable

If you also share your connection, your **provider** client holds a long-lived
Ed25519 keypair whose seed is persisted to local storage
(`sdk/local_state.go:788`, `.device_local_key_material`, JSON
`client_key_seed`; applied at `sdk/device_local_key_material.go:32,90`). Unlike
the window clients, that key is **deliberately preserved across an automatic
logout wipe** — the Android comment is explicit: *"Clear a stale or partial auth
state WITHOUT rotating the device identity. The identity key material is
device-scoped, not session-scoped"*
(`android/.../MainApplication.kt` (`clearStaleAuthState`)). Token refresh, partial-auth recovery
and the 30-day idle re-login therefore produce a new `client_id` **and** a new
`device_id` while the Ed25519 public key stays the same. Only an explicit user
logout rotates it (`sdk/local_state.go:788`).

That key is published, sealed into every contract naming this client as
destination (`server/controller/connect_controller.go:744-770,816-830`), and readable
by anyone: `GET /key/<client_id>` is unauthenticated by design
(`server/api/api.go:222-223`; `server/controller/connect_controller.go:1227`).
So any party — not just a provider you served — can resolve any client id to its
public key and cluster the client ids that share one. For a provider account,
that is a durable cross-session, cross-`client_id`, cross-`device_id` handle.
This is a real cost of providing, and it is not documented anywhere else in the
corpus.

### 9.3 At the operator, everything links

- `client_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:74`).
  A top-level client is deactivated after 30 days idle
  (`TopLevelClientIdleExpiration`, `:2576`) and hard-deleted 30 days after that
  (`NetworkClientReapAfterDeactivate`, `:2556`), cascading to the device row and
  the 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:55-67,238`), refreshed
  by the SDK at half-life — about 12 hours — with jitter
  (`sdk/device_token_manager.go:172-217`). **Refreshing the token does not
  rotate the client id**; the same claim is re-minted
  (`server/model/network_client_model.go:74`).
- `audit_contract_event` records client and provider identity per contract
  (`server/db_migrations.go:325-347`). `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:325-347`) was later reduced to
  `(block_number, client_address_hash, client_id)` (`:2191-2195`), and the live
  partitioned table uses the same three
  (`server/model/network_client_reliability_partition_model.go:194`), with
  `network_id` surviving as an index payload (`:63,631-634`). Retention is 30 days
  (`server/model/network_client_reliability_partition_model.go:52-69`; daily partitions
  dropped whole).
- `network_client.auth_time` is a durable last-seen retained 30 days
  (`server/model/network_client_model.go:2556,2576`); disconnected connection rows
  are reaped at 8 hours (`server/taskworker/work/network_client_work.go:85`).
- Account-level identifiers, at the operator only: cleartext email or phone in
  `network_user.user_auth`, the full third-party IdP JWT for SSO logins, cleartext
  identifiers on every login attempt 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:13-29`). It is the one entry in the five that is a
  fingerprinting surface.

### 9.4 The WireGuard bridge: the tunnel address is NAT'd before egress

Each WireGuard client is allocated a stable private tunnel address from a
shuffled pool of 10,000,000 RFC1918 addresses
(`server/model/network_client_proxy_model.go:774`), written into the config as
the interface address (`:756-771`). That address is durable: it is part of the
peer's config and survives every session.

**It does not reach the providers.** The hosted device substitutes a per-device
address on egress and restores the peer's own on return. The substitute is
taken once at device construction from the same `169.254.0.0/16` pool the app
path's tun uses (`server/proxy/proxy_device.go:954`, via
`connect.TakeLocalIpv4Address`, `connect/tun.go:471`) and returned on close, so
it is per device rather than per account, and it is drawn from the same space
as every other local tunnel address rather than being distinctive. The rewrite
is `natRewriteEgress` (`server/proxy/proxy_device.go:1297`) and
`natRewriteReturn` (`:1314`), over
`connect.RewriteIpv4Source`/`RewriteIpv4Destination`
(`connect/ip_nat_rewrite.go:71,77`), which repair the IPv4 header checksum and
any pseudo-header transport checksum incrementally. Return packets are matched
on the substituted address, since that is what the provider addressed them to
(`receiveWithNotifyNat`, `server/proxy/proxy_device.go:1339`).

Three limits worth stating, because this removes one linkage and not others.
Only the attached peer's own traffic is rewritten, so a device also serving
HTTP or SOCKS leaves those flows untouched. If the local pool is exhausted the
rewrite disables itself rather than dropping traffic, which means the old
behavior is still the failure mode — an availability choice, and the reason the
property is "normally holds" rather than "cannot fail". And every provider in
one window still sees the *same* substituted address for the duration of that
device, exactly as the app path's tun address is shared across a session's
providers (§9.1); what is removed is the cross-session, account-durable handle,
not within-session correlation.

Behavior pinned by `TestWgNatReplacesThePeerAddressOnEgress`,
`TestWgNatRoundTripIsExact`, `TestWgNatLeavesOtherSourcesAlone` and
`TestWgNatReturnIsMatchedOnTheNatAddress`
(`server/proxy/proxy_device_nat_test.go`), and the checksum arithmetic by
twelve tests in `connect/ip_nat_rewrite_test.go` that verify against full
recomputation rather than reproducing the incremental math.

Combined with the fixed `1.1.1.1` resolver (§3.2) and the absence of a sealed
session on that path (§2.4), the WireGuard bridge remains the least private way
to use the network. The apps and SDK are the most private.

## 10. Legal demands

**Jurisdiction.** The counterparty is BringYour, Inc., a **Delaware C
corporation** (`docs/legal/terms.md:8-9` names the entity; the incorporation is
not stated there), with a notice address at 2261 Market Street #5245, San
Francisco, CA 94114 (`docs/legal/terms.md:265-271`). Governing law is Texas and
venue is Harris County, Texas (`docs/legal/terms.md:314-321`) — a deliberate
choice, and not an inconsistency with the California address: the company's
counsel is in Texas. The one gap a reader hits is that the published terms
state neither the incorporating state nor a registered agent, so the corporate
facts needed to serve process cannot be established from them alone. All of it
is US, and all of it is reachable by US compulsory process, including process
that arrives with a gag order. The terms state that the operator "may monitor
and disclose information where required to do so by law or government order"
(`docs/legal/terms.md:204-206`).

**What a demand on the operator reaches.** In descending order of deanonymising
power: who the account is — cleartext email or phone, the third-party IdP JWT for
SSO logins, wallet addresses, and payment records that tie to a real identity via
Stripe, Apple, Google Play or an on-chain `tx_signature`; when it connected and
from which city, per connection; which providers it used and for how many bytes;
and a keyed hash of the /29 or /56 block it connected from, plus the cleartext
source port. Third parties hold more: the payment processors named in §1 hold
identity the operator never stores, and a Solana `tx_signature` resolves to a
wallet on a public chain.

**What a demand does not reach, because it does not exist.** The transfer ledger
has no destination, host, URL, SNI, port or domain field anywhere
(`review/verified/PRIVACY-ENFORCEMENT.md` §1.2). Uploaded support logs are
discarded on receipt. There is no browsing record to produce, on any path,
sealed or not — this is the strongest structural property in this document, and
it holds whether or not anyone behaves.

**What a demand reaches prospectively.** Everything above is about stored
records. A demand that compels future conduct is a different matter, and §7.4 is
where it bites: an operator compelled to intercept a specific user could, today,
substitute or omit provider key material and read that user's traffic even with
Post Quantum Encryption enabled, with the only signal being a device log line.
The sealed session raises the cost of retrospective disclosure. It does not, in
its current form, resist a compelled operator going forward.

**Account deletion is partial.** `RemoveNetwork` is a hand-written delete list,
not a database cascade (`server/model/account_model.go:108`). It removes the
`network_user` rows and their auth records (password, SSO, wallet, seedphrase),
the `network` row and the name index entry, and it schedules a Stripe
unsubscribe. It does not delete `account_payment`, `stripe_customer`,
`apple_subscription_transaction`, completed `solana_payment_intent` rows,
`network_referral`, device rows or audit rows; those age out on their own
schedules — audit events at 180 days
(`server/model/audit_model.go:1018,1052`) — or, for completed on-chain payments,
not at all (`server/model/solana_payment_intent_model.go:96,137` deletes only
intents with a null `tx_signature`). Deletion also *writes* a row: an
`AuditEventTypeNetworkDeleted` event keyed on the deleted `network_id`
(`account_model.go:108`). The privacy policy's "delete their account and
associated personal information" (`docs/legal/privacy.md:69`) is broader than
what the code does.

**There is no warrant canary, no transparency report, and no published
law-enforcement process.** Verified absent across `docs/` and the ur.io site
source. There is no legal-process address at all: `security@ur.io` is scoped to
vulnerability reports (`docs/legal/vdp.md:42`) and `notice@ur.io` is the general
contractual notice address (`docs/legal/terms.md:265`), neither of which is a
channel for service of process. A party wishing to serve the company has nothing
in the published terms to act on beyond the San Francisco notice address.
URnetwork's own comparison documents already concede the missing canary and
transparency report against competitors that publish one, and this document
repeats it rather than softening it.

**The privacy policy is silent where it should be specific.** It states that
"all personal information that we collect from and about users is limited to
e-mail address or telephone number" and that collection happens "directly from
you when you provide it" (`docs/legal/privacy.md:29-37`).

An earlier draft of this section called that an under-description on the ground
that the code stores more categories than the policy names. That framing was
withdrawn on 2026-08-09 as wrong. Everything else this document inventories is
either account data the user creates by signing up or paying (the Stripe join
key exists because someone agreed to be billed), or it is not personal
information at all: a source port, and a keyed block hash the operator does not
reverse. Whether a keyed hash could in principle be inverted by its own holder
is a real engineering caveat — §5 states it, and the missing rotation is a
genuine weakness — but a value nobody looks up is not a category of personal
information the policy failed to declare.

What the policy is actually missing is different and narrower: **no retention
period, no logging statement, and no law-enforcement or government-disclosure
section.** The strings *retention*, *log*, *IP address*, *subpoena* and *legal
process* do not appear in it. That matters because the retention discipline
exists in code and is stronger than the policy claims — §5.1 sets out the
verified windows. A user reading the policy today cannot hold the operator to
any of them.

## 11. What URnetwork does not defend against

Three things hold, and they are worth naming before the list that follows, so the
list is read as a boundary rather than a verdict. On a relayed path a provider
never learns who you are, and no setting or provider build changes that (§2).
There is no destination, host, URL, SNI, port or domain field anywhere in the
transfer ledger, so there is no browsing record to compel, leak or sell (§5,
§10). And every one of these claims is readable in public source, which is why
this document can be specific about its own failures.

Everything below is a failure. Stated without hedging; each item is expanded
above.

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. **An operator that was never honest, substituting provider key material.**
   Substitution now costs a signed, permanent, third-party-detectable artifact,
   and an operator that turns malicious or is inconsistent is caught. One that
   is malicious and self-consistent from your first contact still wins, because
   the client has no operator-independent view of which signer is authoritative.
   §7.4.
5. **No seal at all WITH Post Quantum Encryption OFF, the shipping default.**
   In that mode the client never starts a session, so its traffic is unsealed
   from the start, with no user-visible indication. **Fixed for the mode that
   asks for it, 2026-08-10:** with the toggle on, the client runs fail-closed
   and refuses to send or accept plaintext application data. What survives in
   both modes is the missing indicator — no app shows whether a given
   connection is sealed. §2.2.
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. **Within-session correlation on the WireGuard bridge.** The peer's durable
    tunnel address is NAT'd before egress, so it no longer follows an account
    across sessions — but every provider in one window still sees the same
    substituted address, exactly as they see one tun address on the app path.
    And if the local address pool is exhausted the rewrite disables itself
    rather than dropping traffic, so the pre-NAT behavior remains the failure
    mode. §9.4.
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.

- **Journald retention and forwarding on the hosts.** Service logs are now
  scrubbed at the descriptor before they leave the process (§5), so what
  reaches journald should carry no addresses outside a crash dump. What this
  source review cannot establish is how long journald keeps what it receives,
  where it forwards it, and whether any crash dump — which passes through
  unscrubbed by design — is retained longer than the rest. That is a
  deployment question, not a code one.
- **Whether name resolution on the browser path can ever happen locally.** The
  extension configures an HTTPS CONNECT proxy by default, which resolves remotely,
  and SOCKS name resolution goes through the tunnel's DoH dialer
  (`connect/tun.go:517-523`). Per-browser proxy DNS behaviour under every
  configuration was not tested.
- **Actual production verbosity.** `BY_LOG_V` defaults to 0
  (`server/env.go:46`), 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:3525`), and location is
  derived by the operator from the connection it observes rather than declared by
  the provider (`review/verified/ARCHITECTURE.md` §6.4). But no independent party
  has measured the fleet from outside, and nothing verifies that the providers
  offered to a particular client are independent of one another or of the
  operator. The mechanism is checkable in source; the population is not checkable
  by a third party today.
- **The identity-key binding at account creation.** The design states the
  expectation that "the (ClientId, public key) binding is registered at account
  creation" (`connect/transfer_key.go:60-85`). What ships is a client publishing
  its key to operator-controlled Redis, served back by an operator-controlled API.
  Whether a stronger registration exists operationally could not be established
  from source; assume it does not.
- **Whether `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:547-561`;
  `server/controller/connect_controller.go:831-838`). No evidence was found that
  the apps populate them, and not every caller was audited. If they were ever
  populated for consumer traffic they would be first-class cross-session
  identifiers visible to a provider.
- **Key-material persistence on non-mobile hosts.** `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.

## 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.
