This commit is contained in:
2026-08-16 19:02:48 -03:00
parent 96fe00eb2b
commit c8e3011f21
31 changed files with 3457 additions and 351 deletions
+117 -36
View File
@@ -90,10 +90,11 @@ flow control a matter of reporting a number.
**DragonTCP does not provide authenticated encryption. It is not a VPN in the
security sense.**
Payloads are **masked**: XORed with a keystream derived from SHA-256 that varies
with session ID, mode, sequence, direction, and block number. The same plaintext
therefore does not produce the same ciphertext twice, and no fixed ASCII markers
appear on the wire.
Legacy payloads are **masked**: XORed with a keystream derived from SHA-256 that
varies with session ID, mode, sequence, direction, and block number. New clients
also support a self-described **clear-payload** profile that removes the per-32-byte
SHA-256 work. Auto tries clear first and falls back to the legacy mask for older
servers or networks that reject the clear profile.
This defeats trivial pattern matching. It does not defeat anyone who can read the
traffic:
@@ -101,7 +102,9 @@ traffic:
* The mask is derived from the **session ID, which is transmitted in cleartext in
every request header.** Anyone who sees the header can regenerate the keystream
and recover the plaintext. This is obfuscation, not confidentiality.
* Record headers — mode, session, sequence, length — are never masked.
* Session, sequence, and length fields remain clear. The mode/status byte may
use a startup-selected header mask, but this is traffic shaping rather than
cryptographic protection.
* `StatusError` bodies are sent **unmasked**, as plain text.
* There is no integrity check, so payloads can be tampered with undetected.
@@ -130,6 +133,7 @@ core/
cmd/dragontcp-server/
main.go listener, DNS cache, address filtering, CLI flags
chunk.go session manager, buffering, request dispatch
bhttp.go auto-detected BP/BHP1 transport compatibility
debug.go counters and periodic statistics
chunk_test.go
internal/wire/
@@ -227,7 +231,7 @@ A single request may be answered by **several** responses — see `ModeDownload`
| `StatusWait` | 3 | Nothing available yet; ask again |
| `StatusEOF` | 4 | Target closed the stream |
### 4.3 Payload masking
### 4.3 Payload encoding
```text
seed[0:16] = session ID
@@ -245,6 +249,12 @@ sequence field is a **byte offset** (§4.5), consecutive records never reuse a
keystream position, and re-sending the same offset reproduces the same bytes —
which is what makes idempotent retries safe.
When the cover preface carries the clear-payload flag, request and data bodies are
sent without that transform. Headers, session semantics, retry offsets, and all
payload layouts remain identical. Old clients remain masked and are accepted by
the new server. An old server rejects the new flag, so the new client's next
startup candidate is the corresponding legacy direct profile.
Not everything is masked. `WriteResponse` sends the body as-is and is used for
empty `StatusOK`, `StatusWait`, `StatusEOF`, and every `StatusError`.
`WriteMaskedResponse` is used for `StatusData` and for the `OPEN` result. On the
@@ -350,6 +360,27 @@ client server
│◀─ OK ────────────────────────────────────────│
```
### 4.7 BP transport compatibility
Binary connections are auto-detected as either native Dragon B or the
observable BP protocol used by `bhttp_remote_test.py`. BP support uses the
same 29-byte request header, five-byte response header, and SHA-256 counter mask,
but maps modes as `0=probe`, `1=upload/register`, `2=single download`, `3=batch
download`, and `4=ACK`. It implements `BHP1` version-1 probe integrity, probe
batching, empty mode-1 session registration, upload acknowledgements, mode-2's
header-only size hint, the six-byte batch request, ACK, expiry, and unknown-session
errors. Dragon peers can additionally negotiate the clear-payload encoding via
the cover preface; reference clients continue through the original direct masked
encoding unchanged.
The available reference client does not expose a destination-selection or
authentication exchange. Its observable registration/upload/download/ACK
behavior remains accepted unchanged. Dragon's BP client adds a `DOP1` upload
extension after registration to carry the normal token, target host, and port;
that extension gives the Android app a complete bidirectional stream without
changing the reference client's frames. Native Dragon B remains available and
is not replaced.
---
## 5. The Go client
@@ -404,10 +435,44 @@ keepalives, and optionally explicit buffer sizes via `--tcp-buffer`.
| `--chunk-reconnect-every` | Behaviour |
|---|---|
| `0` | Persistent — one connection for the life of the lane |
| `1` (default) | Auto — persistent if the path probe showed reuse works, otherwise one logical request per connection. Resolved silently, since it runs once per flow |
| `1` (app default) | Auto — starts persistent, retries once after reconnect, and switches that lane to one request per connection only when reuse fails during real traffic |
| `N ≥ 2` | Rotate — close and redial after N logical requests |
### 5.4 Path probing
### 5.4 Startup profile and path probing
Before normal traffic, the client discovers one fixed wire/header profile. The
original direct range remains intact: B provides 32 masks for its mode byte, X
provides 96 masks for its `UP`/`OK` magic, and BP provides its original direct
profile. The cover range adds both legacy-masked and clear-payload B profiles,
clear-payload BP profiles, and covered X profiles. It varies a masked multi-byte
preface, all 256 header-mask/first-byte values, and a distributed set of padding
lengths: `0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 768, 1024, 1400, 2048,
4096` bytes.
Every candidate is tested with real tunnel traffic: the client opens the tunnel,
sends an HTTP request to `http://ip.dr2.site/` over TCP port 80, and accepts the
profile only after receiving an HTTP status line. Discovery no longer rejects a
profile based only on the synthetic `CPROBE`/`DTP2` exchange.
The safe default is one probe thread. `--wire-probe-threads` can allow 116
in-flight attempts, while `--wire-probe-delay` (default 1 second) is still the
global minimum time between new connection starts. Increasing the thread count
therefore permits slow attempts to overlap; it does not launch all candidates at
once. One thread is recommended on carriers with connection-rate filtering.
The winner is cached for the lifetime of the client process and is used
unchanged for every connection and record. Padding contents may be fresh random
bytes, but the selected length and profile never change until restart; there is
no per-packet profile mutation.
`--wire auto` searches 1,153 distributed B/BP/X profiles. Pinning `--wire b`
searches 544 B profiles, `--wire bp` searches 257 BP profiles, and `--wire x`
searches 352 X profiles. B and BP try a clear-payload candidate first, followed
immediately by a legacy direct fallback. A successful selection is logged as:
```text
wire probe: selected=x/mask-6b/cover-91e7/pad-64 completed=141 launched=142 elapsed=42.8s target=http://ip.dr2.site/ validated=true threads=1 fixed_until_restart=true
```
Before the first real connection, `getPathProfile` measures the path once and
caches the result for **30 minutes**, keyed by server address, token, and size
@@ -558,10 +623,11 @@ Source: `core/cmd/dragontcp-server/`.
### 6.1 Connection handling
The server listens on `0.0.0.0:53` by default, bounded by `--max-connections`
through a slot channel. Each connection runs a loop: read one request with a
30-second deadline, dispatch it, repeat. Because session state is keyed by session
ID rather than by connection, requests for one logical stream may arrive over many
connections in whatever pattern the client chooses.
through a slot channel. Each connection runs a loop: read one request with an
idle deadline, dispatch it, repeat. The deadline is refreshed halfway through its
window rather than issuing a system call for every record. Because session state
is keyed by session ID rather than by connection, requests for one logical stream
may arrive over many connections in whatever pattern the client chooses.
### 6.2 Session state and buffering
@@ -574,7 +640,8 @@ target plus a download buffer:
That goroutine **blocks when the buffer is full**, which is the entire flow
control story described in §4.5. Buffer size is `--chunk-buffered × 65536`,
clamped to 1 MiB…64 MiB (default 25616 MiB per session).
clamped to 1 MiB…64 MiB (default 32 → 2 MiB per session). The same byte limit is
enforced for X; it is not multiplied by the negotiated chunk size.
Acknowledgement drops bytes off the front and advances `base`. The buffer is
compacted when its capacity exceeds four times its length and is over 1 MiB, so
@@ -673,9 +740,9 @@ a helper binary in an APK.
`DragonService` launches it with `ProcessBuilder`, merges stderr into stdout, and
reads the output on a background thread. Only interesting lines reach the UI log:
those beginning with `adaptive ` or `path probe:`, and anything containing `error`
or `failed`. A watchdog thread waits on the process; if the core exits while the
tunnel is supposed to be up, the whole VPN is torn down.
those beginning with `wire`, `adaptive `, or `path probe:`, and anything
containing `error` or `failed`. A watchdog thread waits on the process; if the
core exits while the tunnel is supposed to be up, the whole VPN is torn down.
### 7.3 Startup and shutdown
@@ -822,12 +889,15 @@ checksum of zero is written as `0xFFFF` per RFC 768.
| Server | — | `--server-host` |
| Port | 53 | `--server-port` |
| Token | empty | `--token` (omitted entirely when blank) |
| Wire | auto | `--wire`; Auto, B, BP, and X are selectable |
| Probe delay (ms) | 1000 | `--wire-probe-delay`; global minimum delay between profile connection starts |
| Probe threads | 1 | `--wire-probe-threads`; maximum concurrent profile attempts, 116 |
| Max chunk | 1048576 | `--chunk-max` **and** `--chunk-start` |
| Min chunk | 32 | `--chunk-min` |
| Batch max | 1 | `--chunk-concurrency` |
| Batch min | 1 | `--chunk-concurrency-min` |
| Reconnect every | 1 (auto) | `--chunk-reconnect-every` |
| Timeout (s) | 2 | `--chunk-timeout` |
| Timeout (s) | 5 | `--chunk-timeout` |
Fixed by the service: `--listen-host 127.0.0.1`, `--listen-port 8080`,
`--transport chunk`, `--chunk-grow-after 16`, `--chunk-adapt-log=true`.
@@ -853,7 +923,7 @@ Download batch: pinned at 5 records per request (never adapts)
Validation ranges: port 165535, max chunk 321048576, min chunk 32max chunk,
batch values 1256 with `min ≤ max`, reconnect 01000000,
timeout 1120.
timeout 1120, probe delay 20030000 ms, probe threads 116.
### 7.10 Logs
@@ -989,26 +1059,30 @@ keystore out of version control.
### 9.1 Server
```bash
sudo ./dragontcp-hybrid-server-linux-amd64 --port 53 --chunk-max 1048576
sudo ./dragontcp-hybrid-server-linux-amd64 \
--port 53 --port-alt 80 --chunk-max 1048576
```
With a token:
```bash
sudo ./dragontcp-hybrid-server-linux-amd64 \
--token 'YOUR_SECRET' --port 53 --chunk-max 1048576
--token 'YOUR_SECRET' --port 53 --port-alt 80 --chunk-max 1048576
```
With diagnostics:
```bash
sudo ./dragontcp-hybrid-server-linux-amd64 \
--port 53 --chunk-max 1048576 --debug --debug-stats-interval 10s
--port 53 --port-alt 80 --chunk-max 1048576 --debug --debug-stats-interval 10s
```
`sudo` is needed only because port 53 is privileged. If `systemd-resolved` or
`dnsmasq` already owns port 53, free it or choose another port. No TUN device, NAT,
or firewall rules are required.
The server listens on TCP ports 53 and 80 simultaneously by default. Set
`--port-alt 0` to disable the second listener. Failure to bind the primary port
is fatal; failure to bind the secondary port prints a warning and leaves the
primary listener running. `sudo` is normally needed because both defaults are
privileged. If another service owns either port, free it or select a different
port. No TUN device or NAT rules are required.
### 9.2 Client CLI
@@ -1023,7 +1097,7 @@ summarises the active configuration:
```text
adaptive_chunk=true start=1048576 min=32 max=1048576 grow_after=16 pollers=1 \
batch=5-5(pinned) reconnect_every=0 timeout=2s
batch=5-5(pinned) reconnect_every=0 timeout=5s
```
### 9.3 Android
@@ -1041,7 +1115,9 @@ Batch max: 1
Batch min: 1
Pollers: 1
Reconnect every: 1
Timeout (s): 2
Timeout (s): 5
Probe delay (ms): 1000
Probe threads: 1
```
Use **OPEN LOGS** to watch the path probe and any adaptation.
@@ -1055,7 +1131,8 @@ Use **OPEN LOGS** to watch the path probe and any adaptation.
| Flag | Default | Meaning |
|---|---|---|
| `--host` | `0.0.0.0` | Listen address |
| `--port` | `53` | Listen port |
| `--port` | `53` | Primary listen port |
| `--port-alt` | `80` | Simultaneous secondary listen port; 0 disables it |
| `--token` | empty | Optional shared secret |
| `--max-connections` | `20000` | Concurrent TCP connections |
| `--allow-private` | `false` | Allow private/loopback targets — keep off in public |
@@ -1063,7 +1140,7 @@ Use **OPEN LOGS** to watch the path probe and any adaptation.
| `--dns-cache-size` | `4096` | Cached hostnames |
| `--tcp-buffer` | `0` | Explicit socket buffers; 0 = OS autotuning |
| `--chunk-max` | `1048576` | Largest record accepted (32 B 1 MiB) |
| `--chunk-buffered` | `256` | Per-session buffer in 64 KiB units (≈16 MiB) |
| `--chunk-buffered` | `32` | Per-session buffer in 64 KiB units (≈2 MiB) |
| `--chunk-poll-wait` | `200ms` | Long-poll wait for a batch's first record |
| `--chunk-session-timeout` | `2m` | Idle session reaping |
| `--debug` | `false` | Session/connect/error logs plus periodic stats |
@@ -1078,6 +1155,9 @@ Use **OPEN LOGS** to watch the path probe and any adaptation.
| `--server-host` / `--server-port` | — / `53` | Remote server (host required) |
| `--token` | empty | Shared secret |
| `--transport` | `chunk` | Must be `chunk` |
| `--wire` | `auto` | Search B/BP/X; `b`, `bp`, or `x` pins one mode |
| `--wire-probe-delay` | `1s` | Global minimum delay between profile probe starts; range 200ms30s |
| `--wire-probe-threads` | `1` | Maximum concurrent real HTTP profile probes; range 116 |
| `--max-connections` | `20000` | Concurrent proxied connections |
| `--tcp-buffer` | `0` | Explicit socket buffers |
| `--chunk-start` | `1048576` | Initial record size (the probe overrides it) |
@@ -1091,7 +1171,7 @@ Use **OPEN LOGS** to watch the path probe and any adaptation.
| `--chunk-concurrency-min` | `1` | Download batch floor, 1256; equal to the ceiling pins the depth |
| `--chunk-reconnect-every` | `0` | 0 persistent, 1 auto, N rotate |
| `--chunk-poll-delay` | `2ms` | Pause after an empty poll |
| `--chunk-timeout` | `2s` | Per-record transaction timeout |
| `--chunk-timeout` | `5s` | Per-record transaction timeout |
| `--chunk-pollers` | `1` | Accepted for compatibility; validated 1128 but unused |
The `concurrency` flag names are historical. They control the download **batch
@@ -1127,10 +1207,10 @@ manual tuning makes things worse.
high, because each round trip returns more data. If the log repeatedly shows
`adaptive download batch: N -> N/2`, the path cannot sustain that depth.
* **Streams die mid-transfer, or nothing loads at all.** Set `Reconnect every`
to `1` (auto). `0` forces persistent connections, and many networks silently
kill long-lived port-53 connections; auto probes first and falls back to one
logical request per connection when persistence does not survive. This is the
single most important setting on a restrictive path.
to `1` (auto). Auto starts persistent and retries an idempotent request once on
a fresh connection. If reuse itself failed, only that lane switches to one
logical request per connection. This avoids turning one imperfect startup
probe into connection churn for every active tunnel.
* **Logs show the same transition many times over (`128 -> 64` repeatedly).**
Each tunnel adapts independently, so a burst of flows produces a burst of
identical lines. The app collapses consecutive duplicates into a counted line;
@@ -1151,7 +1231,8 @@ manual tuning makes things worse.
a small number of high-BDP connections is `1048576` or `4194304` worth trying;
across many connections it costs memory for nothing.
* **Server memory.** Each session can hold `--chunk-buffered × 64 KiB` (default
16 MiB). Lower it when running many concurrent sessions.
2 MiB). Lower it when running many concurrent sessions; the minimum effective
window is 1 MiB.
---
@@ -1196,8 +1277,8 @@ cd core && go test ./...
Coverage:
* the masking round-trip, and that different sequences produce different wire
bytes,
* legacy masking round-trips and different-sequence wire bytes,
* clear-payload B and BP profiles end to end, including nonzero header masks,
* the adaptive record sizer recovering from the minimum rather than latching
there,
* `reconnectEvery == 0` meaning persistent,