Files
tsunagi/docs/threat-model.md
T
Aleksandr Bogomiakov 2166ba4800 Separate internal WireGuard encryption from data carriers
AWG-only policy also selected a competing end-to-end plugin, so compatible
external links could still have no compatible encrypted data channel. Carrier
changes replaced tunnel state and status selected counters from different
engines. Use one internal WireGuard plugin and a separate DataCarrier contract
for external capabilities/lifecycle. Keep the logical link, keys, handshake,
interface and counter epoch across policy, fallback and direct/relay changes.
Control/discovery transport selection and membership remain independent.

Share the bounded per-neighbour UDP WireGuard engine between ordinary WG and
Amnezia masking. Encrypt the relay envelope inside the outer hop channel with
fresh pair keys/PSK. Remove duplicate WG key/device code and the TCP/TLS inner
cipher. TLS carries opaque internal WireGuard frames. Refuse missing native
transports instead of letting default iroh impersonate their protocol names.
Probe the actual authenticated carrier, independently of the shared IP tunnel;
failed local probe submissions count as loss too.

Apply the vendored iroh interface exclusion to actual direct candidates, not
just public discovery records. Exclude overlay subnets from introductions and
propagate the actual owned interface name, including kernel-assigned utun names,
to every carrier. Production logs showed paths through the overlay itself.
This removes a recursive transport path; it does not establish the historical
cause of the reported 100 GB absolute counter. Exit-node traffic is also real
peer IP traffic even when local applications are idle.

Hide inner cipher choices/handshake clutter in CLI and tray. Report external
routes, readiness and actionable policy/version failures. Count original IP
packets only when their ciphertext is accepted by the logical transport,
including queued packets' exact unpadded lengths. Exclude physical probes and
keepalives. Export counter epochs; sum per-peer rate deltas and discard stale
histories so joins/restarts cannot turn old totals into fresh traffic. Status
also prints exact IP byte totals for comparison with the tray.

Coordinated upgrade: data ALPN 6, internal wireguard-e2e capability v4,
wg/awg v2, QUIC carrier v1, TLS v2, local IPC v23. Control ALPN 8, device/network
identity derivation, signed records and saved network choices stay unchanged.
Keep the historical wg-quic key store; transactionally import missing keys from
wg then awg without replacing canonical keys. Forget removes legacy copies too,
on the blocking pool, to prevent resurrection after restart. Preserve historic
cipher options as shared settings, rejecting contradictory values.

Validation on Linux in the Nix development shell:
- cargo fmt --all -- --check
- cargo clippy --locked --workspace --all-targets -- -D warnings
- cargo test --locked --workspace --all-targets (562 passed, 3 ignored)
The inherited service socket/secret/group environment overrides were removed
for isolated CLI process tests. A new carrier test initially raced the first
peer announcement; its deadline polling now waits for a peer row explicitly.
Real loopback endpoints, membership, SQLite, WG/AWG/TLS encryption and memory
TUNs cover mixed-carrier relaying and one smoltcp TCP connection surviving
relay/direct/policy changes and a dropped transmission. Counter tests check
exact plaintext lengths, refused sends, epochs, joins and stale histories.
Public DHT, privileged kernel and manual benchmark tests remain ignored;
macOS/Windows native TUNs, desktop GUI and WAN/DPI behavior were not run.
2026-10-22 01:07:52 +01:00

137 lines
8.2 KiB
Markdown

# Threat model and known limits
Read this before relying on anything here. The protocol is in
[protocol.md](protocol.md).
## What is protected
- **Network membership.** A peer must prove knowledge of `auth_key`, derived
from the network name and shared secret, to get an authenticated session.
Knowing the public `NetworkId`, or an agent's address, is not enough.
- **Endpoint authenticity.** iroh's QUIC/TLS handshake authenticates the
remote endpoint id, which is its public key. Identities in the membership
transcript are taken from the certificate, never from a peer's claim.
- **Connection binding.** The membership proof includes TLS exporter output, so
a proof captured on one connection does not verify on another.
- **Role separation.** Initiator and responder proofs cover different
transcripts, so a proof cannot be reflected back at its sender.
- **Network isolation.** A session authenticated for network A cannot carry
messages for network B, even over a shared physical connection.
- **Confidentiality and integrity in transit.** Provided by QUIC/TLS. This
crate adds no encryption of its own.
- **Overlay address ownership.** A peer's overlay address is derived from its
public key, not taken from its announcement. Outbound packets go to the owner
of the destination address; inbound packets are dropped unless their source
is the address derived for the peer that sent them. A member can therefore
neither receive nor forge another member's traffic. See
[wireguard.md](wireguard.md#address-ownership-is-enforced-not-announced).
- **Tunnelled traffic is end-to-end encrypted by WireGuard**, independently of
this crate. Carriers may be iroh, TLS, direct WG or
pairwise AWG; confidentiality does not depend on a particular carrier.
- **Resource bounds.** Frame lengths are validated before allocation; strings,
lists, queues, concurrent dials and in-flight handshakes are all bounded;
handshakes, dials and writes have timeouts.
## What is not protected
- **Anyone who knows the secret is a full participant.** They can create
arbitrarily many identities, flood the network with records and collide with
other participants' names. This is why a majority is not a root of trust.
Signatures protect authorship; they do not make a participant honest.
- **Cleanup rights are equal.** Any member can retire an observed signed claim
of another device, freeing its hostname and IPv4 reservation. A valid original
claim bounds the cutoff; a member cannot invent a future cutoff or forge the
target's release. The local API refuses online nodes, but a remote signature
cannot prove that a target was offline. A live target may publish a newer
claim; a malicious member can repeatedly disrupt reservations. Cleanup is
not expulsion or secret revocation. See
[sync-model.md](sync-model.md#offline-node-cleanup).
- **Weak secrets.** This targets high-entropy secrets. There is no PAKE, so a
short human passphrase can be guessed offline by anyone who can reach the
handshake. Use `NetworkSecret::generate()`.
- **Public address publication.** With `TransportPolicy::N0Defaults` or
`DirectOnly`, iroh publishes a signed record of this endpoint's addresses,
keyed by its endpoint id, to the public pkarr/DNS service run by Number 0
("n0", the company behind iroh) at `dns.iroh.link`, and resolves peers
through it. The network secret is never published and
membership cannot be inferred from a single record, but the endpoint's
existence and its addresses become public. `LocalOnly` publishes nothing.
- **Addresses and metadata are observable.** Anyone able to watch the network
sees addresses, timing and volume. Discovery backends see the
`discovery_key` and the addresses published under it, which is enough to map
a network's participants. This library does not make a network anonymous, and
having iroh under it does not make it so.
- **A cloned state directory is a cloned identity.** `state.sqlite` holds the
device secret key and the network secrets. Copying it copies the participant.
Restoring an old backup rolls the agent's state back and can replay stale
versions until it meets a replica holding the newer signed state. Retained
cleanup markers prevent old claims from resurrecting after that merge; they
cannot help if every copy of a marker has been lost.
- **A compromised host.** The secret is on disk to survive restarts. File
permissions are owner-only where the platform supports it, and the state
directory takes an ownership lock, but neither defends against a user who can
read the file or against malware running as that user.
- **User IP traffic.** Carried by the WireGuard plugin over an iroh data
connection, and encrypted by WireGuard end to end. *Filtering* it is still
the operating system's and the user's job: the plugin creates connectivity
between members and does not police what flows over it.
- **Traffic metadata reaches the relay when one is used.** If iroh cannot hole
punch, the data connection goes through a relay, which then sees the volume
and timing of tunnelled traffic — though not its contents, which WireGuard
encrypted, nor the iroh layer's contents.
- **Exit nodes.** A member that offers one carries other members' internet
traffic and sees where it goes; a member that uses one trusts it completely.
Anyone holding the network secret can use an offered exit node. See
[exit-node.md](exit-node.md).
- **Overlay address squatting.** A member can mint many WireGuard keys and
therefore occupy many overlay addresses. It cannot pick which ones, but it
can consume them and appear as many participants.
- **Plugin keys on disk.** The WireGuard private keys live in the plugin's own
`wireguard.sqlite`, owner-only where the platform supports it. Copying that
file copies this agent's overlay identity, exactly as copying `state.sqlite`
copies its control plane identity.
- **What the data plane does not police.** Address ownership stops a member
impersonating another member. It does not stop a member sending whatever it
likes *from its own* address.
- **Denial of service.** Bounds and timeouts stop trivial resource exhaustion
from a single peer. They do not make the agent resistant to a determined
attacker who knows the secret, and no rate limiting per identity exists yet.
- **Global freshness.** A signature proves authorship, not that you have the
newest state. See [sync-model.md](sync-model.md).
- **Discovery is not trustworthy.** It returns candidates. A hostile or stale
discovery backend can waste dial attempts and learn addresses; it cannot
forge membership.
## Deliberate design consequences
- **No owner, no vote.** Nobody can evict anybody. Removing a participant means
changing the secret, which creates a different network space that the removed
participant cannot enter.
- **Rotating the secret is not revocation of past access.** Anyone who held the
old secret keeps whatever they already saw.
- **Local deactivation is not revocation.** Deactivating a network stops this
agent participating. It says nothing about anyone else.
- **Failures are contained, not escalated.** A bad proof, wrong secret,
malformed frame or unknown version rejects one message or one session. It
never stops another network and never stops the agent, and there is no
irreversible global error flag.
## Cryptographic choices
Standard primitives only, no home-made constructions: HKDF-SHA256 (RFC 5869)
for key separation, HMAC-SHA256 for the membership proof, constant-time
verification, iroh's Ed25519 endpoint keys and QUIC/TLS for the transport, and
RFC 5705 TLS exporter output for channel binding. There is no custom encryption
layer and no custom PAKE.
## Pairwise AWG masking
AWG profiles, PSKs and outer keys are private to one authenticated pair attempt,
not shared network state. Address candidates are unverified until authenticated
outer WireGuard data arrives. Header protection and structured signature packets
reduce fixed wire markers; they do not guarantee admission by DPI or hide traffic
analysis. iroh control availability remains a prerequisite. Authenticated physical-link
delivery probes detect sustained loss on that carrier, independently of a
working end-to-end relay path. They never confer membership or enter the TUN. See
[amnezia.md](amnezia.md) for limits and tested scope.