You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: PROTOCOL.md
+62-7Lines changed: 62 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,7 +30,9 @@ flowchart LR
30
30
CS --> CFM2["FlowManager → addr 2"]
31
31
CSM --> CHCP["HealthCheckProvider"]
32
32
CFM1 --> CDP1["DecoyProvider"]
33
+
CFM1 --> CAPH1["ActiveProbeHandler"]
33
34
CFM2 --> CDP2["DecoyProvider"]
35
+
CFM2 --> CAPH2["ActiveProbeHandler"]
34
36
end
35
37
36
38
subgraph Server
@@ -39,7 +41,9 @@ flowchart LR
39
41
L --> SFM1["FlowManager :port 1"]
40
42
L --> SFM2["FlowManager :port 2"]
41
43
SFM1 --> SDP1["DecoyProvider (per user)"]
44
+
SFM1 --> SAPH1["ActiveProbeHandler"]
42
45
SFM2 --> SDP2["DecoyProvider (per user)"]
46
+
SFM2 --> SAPH2["ActiveProbeHandler"]
43
47
L --> SSM1["SessionManager (User A)"]
44
48
L --> SSM2["SessionManager (User B)"]
45
49
SSM1 --> SHCP1["HealthCheckProvider"]
@@ -125,6 +129,7 @@ The tailor is the only part of the message that should be decrypted by the flow
125
129
It always starts at `packet_length - tailor_length - tailor_encryption_overhead` and ends at the end of the packet.
126
130
If the data flag is set, the payload should be read starting from `packet_length - tailor_length - tailor_encryption_overhead - payload_length` and until the start of the tailor.
127
131
If the decoy flag is set, the packet should be discarded right away by the flow manager.
132
+
If tailor decryption or authentication fails, the raw (still-encrypted) packet is forwarded to the flow manager's [active probe handler](#active-probing-protection) instead of being silently dropped.
128
133
129
134
### Fake body
130
135
@@ -193,10 +198,14 @@ sequenceDiagram
193
198
194
199
FM-->>FM: receive UDP datagram
195
200
FM->>FM: Strip FakeHeader + FakeBody
196
-
FM->>FM: Decrypt tailor → verify identity + PN
197
-
FM-->>SM: deliver(body)
198
-
SM->>SM: Decrypt body (session key)
199
-
SM-->>App: recv() → plaintext payload
201
+
alt tailor decryption / authentication ok
202
+
FM->>FM: Decrypt tailor → verify identity + PN
203
+
FM-->>SM: deliver(body)
204
+
SM->>SM: Decrypt body (session key)
205
+
SM-->>App: recv() → plaintext payload
206
+
else tailor decryption / authentication fail
207
+
FM->>FM: ActiveProbeHandler::process(raw packet)
208
+
end
200
209
```
201
210
202
211
The simplest pattern affects data packets: they are sent directly and completely, without any delays, jitter, splitting or combination - as soon as possible, providing maximum efficiency.
@@ -459,6 +468,23 @@ The subheader mode can have these values:
459
468
460
469
By default, subheader mode is chosen with equal probability for every option.
461
470
471
+
### Active probing protection
472
+
473
+
Active probing is a technique used by censors and network auditors to probe a suspected TYPHOON endpoint directly: they send crafted packets and observe whether the server responds in a way that reveals the protocol.
474
+
To resist this, the flow manager must handle packets that fail tailor authentication gracefully rather than closing the socket or producing a distinctive error response.
475
+
476
+
Every flow manager has a single **active probe handler** (one per flow, not per user) that receives every packet the flow manager could not identify:
477
+
478
+
-**Tailor decryption or authentication failure**: any packet whose encrypted tailor cannot be deciphered or whose BLAKE3/AEAD authentication tag does not verify is forwarded to the handler. This covers both random garbage and deliberately crafted probe packets.
479
+
-**Server only — unregistered user**: a non-handshake, non-decoy packet arriving with a valid-looking tailor but an identity not present in the registered-user table is also forwarded. This catches replayed or forged post-handshake packets sent by an active prober who has observed legitimate traffic.
480
+
481
+
The handler receives the raw (still encrypted) wire packet together with the UDP source address (`Some(addr)` on the server, `None` on the client whose socket is already connected to a fixed peer).
482
+
It can choose to silently drop the packet, log it, or send a raw response through the flow manager socket using `ProbeFlowSender::send_raw` — completely bypassing TYPHOON framing — to mimic whatever protocol the operator wants to impersonate.
483
+
484
+
The default implementation (`NoopProbeHandler`) drops all unidentified packets silently.
485
+
Custom handlers implement `ActiveProbeHandler<AE>` directly; the `start` method receives a `Weak<dyn ProbeFlowSender>` (for raw send access) and an `Arc<Settings<AE>>` so that expensive construction is deferred to startup and the hot `process` path stays allocation-free.
486
+
Handler instances are created by a `ProbeFactory<AE>` — a simple no-arg closure stored in `ServerFlowConfiguration` or `ClientSocketBuilder` — and attached to the flow manager during `ServerFlowManager::new` / `ClientFlowManager::new`.
487
+
462
488
## Cryptography
463
489
464
490
The following requirements are taken into account for TYPHOON protocol cryptography suite selection:
F -- DATA or SHADOWRIDE --> G[Slice ciphertext via payload_length\ndecrypt_payload with session AEAD key]
570
600
G --> H([Plaintext delivered to application])
571
601
F -- HEALTH_CHECK only --> I([Feed to HealthProvider])
572
602
F -- TERMINATION --> J([Session teardown])
603
+
F -- DECOY --> K([Silently discard])
573
604
```
574
605
575
606
### Handshake encryption
@@ -776,6 +807,7 @@ In short, these are the main TYPHOON implementation parts:
776
807
- Health check provider (one per session): attached to session controller, keeps internal protocol state, manages handshake message timers and injects handshake messages themselves if necessary.
777
808
- Flow controller (one per flow): accepts data, prepends a mock header to it and sends it to the flow partner using a UDP socket.
778
809
- Decoy provider (one per flow): attached to flow controller, observes (and probably mutates) flow packet stream and injects decoy packets whenever necessary.
810
+
- Active probe handler (one per flow): attached to flow controller, receives every packet that could not be identified (tailor authentication failed, or — on the server — packet from an unregistered user), and may send a raw response via the flow socket to mimic another protocol.
779
811
780
812
#### Identification and rebinding
781
813
@@ -1153,6 +1185,11 @@ This design allows each flow manager (or each per-user slot within a server flow
1153
1185
The construction trait `DecoyCommunicationMode<T, AE>` extends `DecoyProvider` with a single `new()` constructor and is the target of `decoy_factory::<T, AE, DP>()`.
1154
1186
`random_decoy_factory()` selects randomly among all five built-in providers on every invocation and is the default when no override is supplied.
1155
1187
1188
+
Active probe handlers implement `ActiveProbeHandler<AE>` (generic in the async executor, object-safe because `AE` is fixed at the usage site) and are constructed through a `ProbeFactory<AE>` — a simple no-arg closure `Arc<dyn Fn() -> Box<dyn ActiveProbeHandler<AE>> + Send + Sync>`.
1189
+
Unlike decoy providers, which receive their context at construction time, probe handlers receive a `Weak<dyn ProbeFlowSender>` and an `Arc<Settings<AE>>` in a single async `start()` call made after the flow manager's `Arc` is fully initialised (post `Arc::new_cyclic`).
1190
+
`ProbeFlowSender` is the raw-send interface exposed to handlers: `ClientFlowManager` implements it by calling `Socket::send` (ignoring the target address, since the socket is already connected), while `ServerFlowManager` calls `Socket::send_to` with the supplied `SocketAddr`.
1191
+
`probe_factory::<AE, PH>()` constructs a factory for any `PH: ActiveProbeHandler<AE> + Default + 'static`, and `NoopProbeHandler` (the default when no factory is supplied) is a zero-cost `Default`-derived stub.
1192
+
1156
1193
### Session module
1157
1194
1158
1195
The `session` module owns session lifecycle, health checks, and the user-visible data pipe.
@@ -1316,3 +1353,21 @@ Certificates in such a setup could either embed a stable relay address (the list
1316
1353
**The challenge**: Dynamic plug/unplug of flow managers while sessions are live requires atomic consistency guarantees between the session state (held by the listener) and the per-flow address tables.
1317
1354
Any message routed to a flow manager that has just gone offline must be either retried on another flow or transparently dropped, while avoiding split-brain scenarios where the listener believes a flow is active but packets are silently lost.
1318
1355
The synchronization protocol, failure detection timeout, and certificate invalidation strategy all need careful co-design to keep the overall system both correct and efficient.
1356
+
1357
+
### Periodic server fake header/body mode rotation
1358
+
1359
+
In the current design, the fake header field layout and fake body mode for each flow are fixed at construction time (encoded in `FlowConfig`) and never change for the lifetime of the flow.
1360
+
A long-lived session therefore produces traffic with a statistically consistent structure — the distribution of packet lengths and of fake header field widths remains constant — which may allow a patient observer to fingerprint TYPHOON flows over time even without breaking the cryptography.
1361
+
1362
+
Because fake headers and fake bodies are purely decorative and do not affect how the receiving side locates or parses the tailor (the tailor position is always derived from known length constants, not from the fake header contents), neither side needs to know or agree on the other side's current configuration.
1363
+
Each flow manager can therefore rotate its own `FlowConfig` independently and at its own pace — switching to a freshly sampled layout after a random interval — without any protocol signaling or cross-side coordination.
1364
+
1365
+
This is analogous to the _protocol polymorphism_ mechanism in [OBFS4](https://gitlab.com/yawning/obfs4/-/blob/master/doc/obfs4-spec.txt), where the server periodically shifts its observable traffic characteristics to frustrate long-term statistical classifiers.
1366
+
The key difference is that OBFS4 achieves this by exchanging explicit polymorphism messages on the wire, whereas TYPHOON requires no such messages: since the two sides are fully decoupled, each can resample its `FlowConfig` unilaterally using `FlowConfig::random`, with the change taking effect on the very next outgoing packet.
1367
+
1368
+
Rotation is opt-in and configured per flow: `FlowConfig` carries an optional rotation interval range — `None` disables rotation entirely, while `Some((min_ms, max_ms))` enables it and bounds the random delay between successive resamples.
1369
+
`FlowConfig::random` enables rotation with `TYPHOON_FLOW_ROTATION_PROBABILITY` probability, sampling the interval bounds from `[TYPHOON_FLOW_ROTATION_INTERVAL_MIN, TYPHOON_FLOW_ROTATION_INTERVAL_MAX]`.
1370
+
1371
+
**The challenge**:
1372
+
The rotation interval itself must not become a new fingerprint.
1373
+
A fixed period (e.g. every _N_ seconds) would create detectable periodic discontinuities in the packet-length or field-width distributions.
0 commit comments