# DAMM Transport Tiers

The canonical spec for the obfuscation ladder. Each tier is a way for a client packet to reach our gateway. They are not a ladder we climb under pressure; they are **parallel options** advertised in the catalog. The score loop and the user pick what works.

**One ground rule for honesty:** the percentages of "what defeats what" are estimates against published adversary capability as of mid-2026. They are not guarantees. Every tier eventually loses to a sufficiently motivated adversary; what each tier buys is *time and effort*.

## The tiers, side-by-side

| Tier | Wire shape | Throughput | Latency | Defeats | Client app needed | Server runtime |
|---|---|---|---|---|---|---|
| T0 | Bare WG/UDP/51820 | full | minimal | nothing in censored networks | stock WireGuard | wg-quick (shipped) |
| T1 | AmneziaWG (junk-pre + magic-rewrite) | ~98% | +1-2ms | static DPI signatures | Amnezia VPN | amneziawg-go |
| T2 | WG over Shadowsocks-TCP | ~80% | +20-60ms | UDP blocking, basic TCP DPI | Amnezia VPN | shadowsocks-rust |
| T3 | WG over WSS/443 | ~70-80% | +40-100ms | SNI block, most TLS DPI | Amnezia VPN | wstunnel + Caddy |
| T4 | T3 + CDN-fronted SNI | ~60-70% | +80-200ms | full DPI except CDN-blocking | Amnezia VPN | Cloudflare account |

T0 is what's deployed (v0.3.2). Everything else: spec, with tracer-bullet implementations described below.

## Domain architecture for censored regions

The single highest-leverage architectural decision for censored-region access is **the domain users connect to**. `vpn.damm.raindesk.dev` contains `vpn` and `damm` — both red flags. We have two clean alternatives:

- **`raindeck.dev`** — `.dev` is HSTS-preloaded by the Google `.dev` TLD policy, so HTTPS-everywhere is enforced; censors usually don't blanket-block `.dev`. The name reads as "developer project," not infrastructure.
- **`hyle.lol`** — backup. `.lol` is less common, more attention-grabbing, but workable.

The proposed domain assignment:

| Domain | Purpose | Tier |
|---|---|---|
| `damm.raindesk.dev` | Public landing + wizard, EU-friendly | T0 entry point |
| `cp.damm.raindesk.dev` | Control plane API | All tiers |
| `vpn.damm.raindesk.dev:51820` | T0 gateway endpoint | T0 |
| `notes.raindeck.dev:443` | T3 cover surface, looks like a blog | T3 |
| `cdn.raindeck.dev` (CNAME → Cloudflare) | T4 fronted endpoint | T4 |
| `*.hyle.lol` | reserve for a second cover surface if `raindeck.dev` gets flagged | T3 backup |

Operator discipline: **never link from `raindeck.dev` content to `damm.raindesk.dev`**. The cover surface should look like a real, mostly-static developer page that has nothing to do with VPN. The wizard should reference `raindeck.dev` only by the wire endpoint, never as a clickable link.

## T1 — AmneziaWG

### What it does

Prepends N junk packets before the real WireGuard handshake. Rewrites the magic header byte (0x01 → some other value, encoded in client config). Adds variable-length junk fields to the `Initiation` and `Response` messages. The result is a stream that doesn't match the well-known WG handshake signature.

### Client config additions

In `[Interface]`:
```
Jc = 4              # junk packet count (1-128 typical)
Jmin = 50           # min junk size, bytes
Jmax = 1000         # max junk size, bytes
S1 = 0              # junk size header init message
S2 = 0              # junk size response message
H1 = 1              # magic header for init (was 1)
H2 = 2              # magic header for response (was 2)
H3 = 3              # magic header for cookie (was 3)
H4 = 4              # magic header for transport (was 4)
```

The `Jc/Jmin/Jmax` tuple controls junk packet generation. `S1/S2` add extra random padding to handshake messages. `H1-H4` rewrite the header byte that DPI signatures key on.

When `H1=1, H2=2, H3=3, H4=4` and `Jc=0, S1=0, S2=0`, AmneziaWG is wire-compatible with vanilla WireGuard. So a single server can serve both T0 and T1 clients on the same port — T0 clients just don't activate the obfuscation features.

### Server-side runtime

Two options:

1. **`amneziawg-go`** — userspace fork of `wireguard-go`. Single Go binary, runs as a daemon, owns a `wgT1` interface. Pros: no kernel module, easy install. Cons: ~30% throughput hit vs kernel WG.
2. **`amneziawg-tools` + kernel module** — DKMS-installable. Pros: full kernel speed. Cons: kernel module, requires reboot or careful loading, harder to roll back.

For tracer-bullet, we recommend **`amneziawg-go`** alongside the existing kernel `wg-quick@wg0`. The two listen on different ports and serve different peer sets. Same gateway keypair (encryption is identical; only the framing differs). Same control-plane state — peers' `frontdoor` field tells the catalog which port to advertise.

### Catalog representation

```json
{
  "id": "fd-eu-hub2-amnezia",
  "transport": "amneziawg",
  "endpoint": "vpn.damm.raindesk.dev:51821",
  "active": true,
  "priority": 20,
  "params": {
    "Jc": 4, "Jmin": 50, "Jmax": 1000,
    "S1": 0, "S2": 0,
    "H1": 5, "H2": 6, "H3": 7, "H4": 8
  }
}
```

Clients that don't speak AmneziaWG should ignore unknown `transport` values and fall back to the next-priority frontdoor.

## T3 — WireGuard over WSS

### What it does

Tunnels WG UDP packets inside a WebSocket-over-TLS stream on port 443. From the network's perspective, the client is making a long-lived HTTPS connection to a website. The TLS connection negotiates with a real cert (Caddy ACMEs it for us). The WSS upgrade looks like any websocket upgrade. Every WG packet becomes a WSS message frame.

### Server-side runtime

Two layers:

1. **Caddy front** — terminates TLS for `notes.raindeck.dev`, serves a real-looking landing page on `/`, proxies `/_w` (or whatever obscure path we pick) to the WSS bridge.
2. **WSS-WG bridge** — accepts WSS connections, reads each incoming WS frame as a WG packet, writes it to the kernel `wg0` UDP listener via a UNIX raw socket (or an internal UDP forward).

Existing tools that do most of this: **`wstunnel`** (Erebe/wstunnel on GitHub) is a Rust client/server that provides exactly this primitive; we'd use it as the back-half of T3.

### Wire layout

```
client (Amnezia VPN)
   │ HTTPS handshake → notes.raindeck.dev:443 (Caddy)
   │ WSS upgrade → /_w
   │ WS frames carrying WG packets
   ▼
caddy
   │ reverse_proxy /_w → 127.0.0.1:8443
   ▼
wstunnel server (or our own bridge)
   │ unwrap WS, forward UDP to 127.0.0.1:51820
   ▼
wg0 (kernel WG)
```

### Catalog representation

```json
{
  "id": "fd-eu-hub2-wss",
  "transport": "wss",
  "endpoint": "wss://notes.raindeck.dev/_w",
  "active": true,
  "priority": 30,
  "params": {
    "tlsServerName": "notes.raindeck.dev",
    "wsPath": "/_w"
  }
}
```

### Cover surface

`https://notes.raindeck.dev/` must serve real-looking content — a blog, a docs site, anything plausible. If a censor probes the URL and gets `404 Not Found` or `502 Bad Gateway`, that's a signal. The cover content can be static; even a copy of someone's GitHub Pages site is better than nothing.

## T4 — Domain-fronted CDN

### What it does

T3's wire shape, but the TLS SNI says `cloudflare.com` (or whatever fronting host) while the HTTP `Host` header says our backend `notes.raindeck.dev`. Cloudflare routes by Host header. From the censor's perspective, the connection is to Cloudflare's edge IPs at the IP level, with a Cloudflare SNI at the TLS level — indistinguishable from any other CF-hosted site.

### What's required

- Cloudflare account with a non-throwaway billing relationship (free tier allows it but restrictively)
- DNS for `notes.raindeck.dev` proxied through CF (orange cloud)
- A Worker or page rule that forwards specific paths to our origin
- Origin IP not directly exposed (CF acts as the only public face)
- Compliance with CF's abuse policy (the cover surface must not look obviously like circumvention)

### Catalog representation

```json
{
  "id": "fd-eu-hub2-cdn",
  "transport": "wss",
  "endpoint": "wss://cdn.raindeck.dev/_w",
  "active": true,
  "priority": 40,
  "params": {
    "tlsServerName": "cdn.raindeck.dev",
    "wsPath": "/_w",
    "fronting": "cloudflare"
  }
}
```

The client config is identical to T3; the `fronting` hint tells the client to expect a CDN edge IP, which it should resolve via standard DNS (CF will DNS-respond with an edge IP).

### Cost shape

CF charges egress at $0.045/GB after a generous free tier. For ~100 users moving 50GB/month each = 5TB/mo = ~$225/mo egress. For ~5 users moving 5GB each = 25GB/mo = $0 (under free tier). Friend-network scale is essentially free; wider service is a real budget line.

## How the catalog and clients interact

**Today** (v0.3.2):
- The catalog publishes one frontdoor per gateway: `transport: "wireguard-udp"`, endpoint `vpn.damm.raindesk.dev:51820`.
- Clients see one frontdoor and use it.

**With T0+T1+T3+T4** (post-v0.3.5):
- Catalog publishes 4 frontdoors per gateway, ordered by priority (T0 is highest priority for unrestricted networks; T1-T4 progressively higher numbers).
- Client behavior:
  - Stock WireGuard clients only understand `transport: "wireguard-udp"`. They get T0.
  - Amnezia clients understand all four. They try T0 first; if the handshake doesn't complete in N seconds, they try T1; and so on.
  - The catalog's `priority` is a *hint*, not a mandate. A client in a known-bad region can be told (via the wizard's region hint) to start at T3 and skip T0/T1.
- The score loop measures per-frontdoor handshake success rate, demotes the failing tiers, promotes the recovering ones.

## Per-frontdoor scoring

The orchestrator's score loop today scores per gateway. With multiple transports per gateway, scoring is per (gateway × frontdoor). A T0 frontdoor that's been blocked in a region scores 0.0; a T1 frontdoor on the same gateway might score 0.8. The catalog-builder uses these to filter and order.

This requires per-frontdoor probes from inside affected networks. We don't have that yet (premortem §10). Until we do, we score every frontdoor identically: based on the gateway's heartbeat and registration. A T1 frontdoor could be objectively dead (nobody can reach it) and our scoring wouldn't notice.

## Tracer bullet status

| Tier | Spec | Catalog data shape | Server runtime | Client app | Score signal |
|---|---|---|---|---|---|
| T0 | shipped | shipped | shipped | shipped | shipped (heartbeat-derived) |
| T1 | shipped | tracer (this release) | **NOT shipped** (`amneziawg-go` not installed) | tracer (config rendering shipped) | shipped (heartbeat-derived) |
| T3 | shipped | tracer (this release) | tracer (`gateway/agents/wss-tracer.js` listens, logs) | unspecced | not shipped |
| T4 | shipped | unspecced | unspecced | unspecced | unspecced |

`tech-debt.md` enumerates the "NOT shipped" items per tier as actionable debt rows.

## Field reality summary

What you can tell a friend, today vs. after each tracer becomes real:

| Region | Today | Post-v0.3.5 (T1) | Post-v0.3.6 (T3) | Post-v0.4.x (T4) |
|---|---|---|---|---|
| Most EU/US | works | works | works (slower) | works (slowest) |
| Iran (typical ISP) | usually fails | ~60% success | ~80% | ~95% |
| Russia (TSPU regions) | usually fails | ~50% | ~70% | ~95% |
| China (GFW) | always fails | ~5% | ~30% | ~60% (and active probing kills T4 slowly) |
| Unrestricted hotel/cafe (UDP often blocked) | sometimes fails | similar (still UDP) | works | works |

These numbers are field estimates from public reporting on similar tools. We will be able to replace them with measured numbers once we have client-side score reporting from inside affected regions.

## What's deliberately not in this spec

- **OSI stack collapsing** (WG-in-DNS, WG-in-ICMP). Throughput collapses to single-digit kbps; niche tool, not a tier.
- **Snowflake / pluggable transports.** Tor-ecosystem; if a friend genuinely needs this they should use Tor, not DAMM.
- **"Trust this domain has CF protection" attestation.** Domain-fronting attestation is an open research problem; we punt to "user trusts the wizard's catalog signature."
- **Client-side polymorphic transport.** A future T5 might rotate between transports mid-flow; out of scope for v0.x.
