1025 lines
41 KiB
Markdown
1025 lines
41 KiB
Markdown
# DragonTCP Hybrid
|
||
|
||
DragonTCP is a tunnelling proxy that carries ordinary TCP traffic inside a
|
||
compact binary record protocol, normally over TCP port 53. It is made of three
|
||
pieces that all live in this repository:
|
||
|
||
* a **Go server** that runs on a Linux VPS and relays streams to their real
|
||
destinations,
|
||
* a **Go client** that runs on the phone and exposes a local HTTP/HTTPS proxy,
|
||
* an **Android app** that captures all device traffic with `VpnService` and
|
||
feeds it into that local proxy.
|
||
|
||
This document explains the whole system: the wire format, both Go programs, the
|
||
Android app, how to build everything (Windows and Linux), how to run it, and how
|
||
to tune it.
|
||
|
||
---
|
||
|
||
## Table of contents
|
||
|
||
1. [What it does and why](#1-what-it-does-and-why)
|
||
2. [Security model — read this](#2-security-model--read-this)
|
||
3. [Repository layout](#3-repository-layout)
|
||
4. [The wire protocol](#4-the-wire-protocol)
|
||
5. [The Go client](#5-the-go-client)
|
||
6. [The Go server](#6-the-go-server)
|
||
7. [The Android app](#7-the-android-app)
|
||
8. [Building on Windows](#8-building-on-windows)
|
||
9. [Building on Linux and macOS](#9-building-on-linux-and-macos)
|
||
10. [Running the server](#10-running-the-server)
|
||
11. [Running the client](#11-running-the-client)
|
||
12. [Tuning guide](#12-tuning-guide)
|
||
13. [Troubleshooting](#13-troubleshooting)
|
||
14. [Version history](#14-version-history)
|
||
15. [Testing and validation](#15-testing-and-validation)
|
||
16. [Licensing](#16-licensing)
|
||
|
||
---
|
||
|
||
## 1. What it does and why
|
||
|
||
### The data path
|
||
|
||
```text
|
||
Android apps (any app, unmodified)
|
||
|
|
||
| IP packets
|
||
v
|
||
Android VpnService TUN interface (10.77.0.2/32, MTU 1400)
|
||
|
|
||
| userspace TCP/IP reassembly
|
||
v
|
||
TunnelEngine (Kotlin, in-process)
|
||
|
|
||
| HTTP CONNECT to 127.0.0.1:8080
|
||
v
|
||
dragontcp-client (Go, child process on the phone)
|
||
|
|
||
| DragonTCP binary records over TCP/53
|
||
v
|
||
dragontcp-server (Go, on the VPS)
|
||
|
|
||
| plain TCP
|
||
v
|
||
destination website
|
||
```
|
||
|
||
The important architectural decision is that the **server is only a relay**. It
|
||
does not create a TUN device, does not do NAT, and needs no `iptables` rules.
|
||
All the packet-level work happens on the phone, in userspace. This keeps the
|
||
server trivial to deploy (a single static binary) and keeps the phone side
|
||
independent of what the server can do.
|
||
|
||
### Why records instead of a raw stream
|
||
|
||
A plain TCP tunnel sends a continuous byte stream. DragonTCP instead splits each
|
||
direction into independent **records**, each carried by its own request/response
|
||
exchange. That costs a little efficiency and buys two things:
|
||
|
||
1. **Record size is negotiable at runtime.** Some networks silently drop or
|
||
truncate large writes on port 53. Because every record is framed and
|
||
acknowledged, the client can discover the largest size that survives the path
|
||
and adapt when conditions change.
|
||
2. **The transport survives connection rotation.** Session state lives in a
|
||
16-byte session ID, not in the TCP connection. The client can close and
|
||
reopen the underlying TCP connection between any two records without losing
|
||
the stream, which matters on middleboxes that cap how long a port-53
|
||
connection may live or how many requests it may carry.
|
||
|
||
---
|
||
|
||
## 2. Security model — read this
|
||
|
||
**DragonTCP does not provide authenticated encryption. Do not treat it as a VPN
|
||
in the security sense.**
|
||
|
||
What it actually does to payload bytes is **masking**: every payload is XORed
|
||
with a keystream derived from SHA-256. The keystream varies with session ID,
|
||
mode, sequence, direction, and block number, so the same plaintext does not
|
||
produce the same ciphertext twice, and there are no fixed ASCII markers such as
|
||
`UP`, `OK`, `CPUSH`, or `CPULL` on the wire.
|
||
|
||
That defeats trivial pattern matching. It does **not** defeat an adversary who
|
||
can read the traffic, because:
|
||
|
||
* the mask is derived from the **session ID, which is sent 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,
|
||
* `StatusError` bodies are sent **unmasked**, in plain text,
|
||
* there is no integrity check, so a network attacker can tamper with payloads
|
||
undetected.
|
||
|
||
The optional `--token` is a shared secret compared in constant time. It gates
|
||
who may open sessions. It is not a key — it does not affect the mask.
|
||
|
||
**Practical consequence:** keep using TLS end to end. HTTPS through DragonTCP is
|
||
protected by HTTPS, not by DragonTCP. Never send plaintext credentials through a
|
||
plain-HTTP site over this tunnel and assume they are private.
|
||
|
||
---
|
||
|
||
## 3. Repository layout
|
||
|
||
```text
|
||
core/
|
||
go.mod module "dragontcp", Go 1.22, zero dependencies
|
||
cmd/dragontcp-client/
|
||
main.go local HTTP/HTTPS proxy, CLI flags
|
||
chunk.go the client transport: lanes, probing, adaptation
|
||
chunk_test.go adaptive sizer + reconnect tests
|
||
cmd/dragontcp-server/
|
||
main.go listener, DNS cache, address filtering, CLI flags
|
||
chunk.go session manager, buffering, request dispatch
|
||
debug.go optional counters and periodic statistics
|
||
chunk_test.go
|
||
internal/wire/
|
||
protocol.go the record format and the masking keystream
|
||
protocol_test.go mask round-trip and sequence-variance test
|
||
internal/protocol/
|
||
protocol.go TCP tuning, relays, legacy UP/OK framing
|
||
xor_*.go word-at-a-time XOR helpers (legacy path)
|
||
|
||
android/
|
||
AndroidManifest.xml package com.dragontcp.client, minSdk 29
|
||
src/com/dragontcp/client/ Java: UI, VpnService, log screen
|
||
src/tech/xvanturing/freeproxy/ Kotlin: userspace TCP/IP stack (Apache 2.0, see §16)
|
||
res/ icon + theme
|
||
assets/ license texts shipped inside the APK
|
||
lib/arm64-v8a/libdragontcp_client.so the Go client, packaged as a native lib
|
||
build_apk.ps1 / build_apk.cmd Windows build
|
||
build_apk.sh Linux/macOS build
|
||
|
||
build_core.ps1 / build_core.sh Go builds (Windows / Unix)
|
||
build_all.sh core + APK in one step (Unix)
|
||
bin/ built server binaries
|
||
licenses/ full Apache 2.0 text
|
||
THIRD_PARTY_NOTICES.md upstream attribution (required — do not delete)
|
||
```
|
||
|
||
Note that `core/internal/protocol` still contains the **legacy** `UP`/`OK` text
|
||
framing and the fixed `0xAD` XOR. That code is retained because
|
||
`internal/protocol` also holds the TCP tuning helpers and relay loops that the
|
||
current transport uses. The legacy framing itself is unreachable in normal
|
||
operation: `dragontcp-client` refuses to start unless `--transport chunk`.
|
||
|
||
---
|
||
|
||
## 4. The wire protocol
|
||
|
||
Everything below is implemented in `core/internal/wire/protocol.go`.
|
||
|
||
### 4.1 Record framing
|
||
|
||
Every client-to-server message is a **request**:
|
||
|
||
```text
|
||
offset size field
|
||
0 1 mode
|
||
1 16 session ID
|
||
17 8 sequence (big-endian uint64)
|
||
25 4 payload length (big-endian uint32)
|
||
29 n payload (masked)
|
||
```
|
||
|
||
Every server-to-client message is a **response**:
|
||
|
||
```text
|
||
offset size field
|
||
0 1 status
|
||
1 4 body length (big-endian uint32)
|
||
5 n body (masked, except where noted)
|
||
```
|
||
|
||
Header sizes are therefore **29 bytes** and **5 bytes**. The hard payload ceiling
|
||
in the wire layer is 2 MiB (`MaxPayload`); the transport never exceeds 1 MiB.
|
||
|
||
### 4.2 Modes and statuses
|
||
|
||
| Mode | Value | Meaning |
|
||
|---|---|---|
|
||
| `ModeProbe` | 0 | Path measurement; no session required |
|
||
| `ModeOpen` | 1 | Create a session and connect to the target |
|
||
| `ModeUpload` | 2 | Push payload bytes toward the target |
|
||
| `ModeDownload` | 3 | Request buffered bytes coming back |
|
||
| `ModeClose` | 4 | Tear the session down |
|
||
|
||
| Status | Value | Meaning |
|
||
|---|---|---|
|
||
| `StatusOK` | 0 | Success; body may carry a result |
|
||
| `StatusError` | 1 | Failure; body is a **plaintext** message |
|
||
| `StatusData` | 2 | Body is stream data |
|
||
| `StatusWait` | 3 | Nothing available yet; poll again |
|
||
| `StatusEOF` | 4 | Target closed the stream |
|
||
|
||
### 4.3 The masking keystream
|
||
|
||
```go
|
||
seed[0:16] = session ID
|
||
seed[16] = mode
|
||
seed[17:25] = sequence (big-endian)
|
||
seed[25] = 1 for responses, 0 for requests
|
||
seed[26:30] = block counter (big-endian, increments every 32 bytes)
|
||
|
||
keystream_block[i] = SHA256(seed)
|
||
payload ^= keystream
|
||
```
|
||
|
||
Masking is its own inverse, so the same call encodes and decodes. Because the
|
||
sequence field is the **byte offset within the stream** (see below), consecutive
|
||
records never reuse a keystream position, and retransmitting the same offset
|
||
reproduces the same bytes — which is what makes idempotent retries safe.
|
||
|
||
Not everything is masked. `WriteResponse` sends the body unmasked and is used for
|
||
`StatusOK` with no body, `StatusWait`, `StatusEOF`, and all `StatusError`
|
||
messages. `WriteMaskedResponse` is used for `StatusData` and for the `OPEN`
|
||
result. On the client, `DecodeMaskedResponse` deliberately skips decoding when
|
||
the status is `StatusError`, so the two sides agree.
|
||
|
||
### 4.4 Payload layouts per mode
|
||
|
||
**PROBE** (`ModeProbe`) — request payload:
|
||
|
||
```text
|
||
offset size field
|
||
0 4 magic "DTP2"
|
||
4 1 probe kind
|
||
5 2 token length (big-endian uint16)
|
||
7 4 value (big-endian uint32)
|
||
11 t token
|
||
11+t … filler, byte i = (i*31 + 17) & 0xFF
|
||
```
|
||
|
||
| Probe kind | Value | Server behaviour |
|
||
|---|---|---|
|
||
| `ProbeUpload` | 1 | Replies `OK` if the whole record was received and is within `--chunk-max`. The *filler* is the thing being measured. |
|
||
| `ProbeDownload` | 2 | Replies `StatusData` with exactly `value` bytes of the same generated pattern. |
|
||
| `ProbeKeepalive` | 3 | Replies `OK`. Used to test whether a connection may carry several requests. |
|
||
| `ProbeBatch` | 4 | Replies with up to 16 back-to-back 32-byte `StatusData` records. |
|
||
|
||
The client verifies download probes byte for byte, so a middlebox that truncates
|
||
or rewrites the response fails the probe rather than silently corrupting data.
|
||
|
||
**OPEN** (`ModeOpen`) — request payload:
|
||
|
||
```text
|
||
offset size field
|
||
0 2 token length
|
||
2 2 host length
|
||
4 2 target port
|
||
6 t token
|
||
6+t h target host (name or literal IP)
|
||
```
|
||
|
||
Response is `StatusOK` with a masked 4-byte body: the server's `--chunk-max`.
|
||
The client immediately clamps its own maximum to that value. Re-sending `OPEN`
|
||
for an existing session is idempotent and just returns the same limit again.
|
||
|
||
**UPLOAD** (`ModeUpload`) — the sequence field is the **byte offset in the upload
|
||
stream**, and the payload is the data. The server requires `offset` to equal
|
||
exactly what it expects next. Response is an empty `StatusOK` ACK.
|
||
|
||
**DOWNLOAD** (`ModeDownload`) — the sequence field is the **byte offset the
|
||
client wants next**. The 14-byte payload is:
|
||
|
||
```text
|
||
offset size field
|
||
0 8 ack offset — everything below this has been consumed
|
||
8 4 maximum bytes per record
|
||
12 2 how many records the client will accept in this batch
|
||
```
|
||
|
||
The server replies with a *stream* of responses to that single request: up to
|
||
`count` `StatusData` records, each masked with the running offset, terminated
|
||
early by a single `StatusWait` or `StatusEOF`. This is the batching mechanism —
|
||
one request, many records.
|
||
|
||
**CLOSE** (`ModeClose`) — no payload; the server drops the session and replies
|
||
`StatusOK`.
|
||
|
||
### 4.5 A complete session, end to end
|
||
|
||
```text
|
||
client server
|
||
|-- PROBE upload (binary search) ---------->|
|
||
|<- OK / error ---------------------------------|
|
||
|-- PROBE download (binary search) ---------->|
|
||
|<- DATA(pattern) ------------------------------|
|
||
|-- PROBE keepalive x8 on one connection ----->|
|
||
|<- OK x8 -------------------------------------|
|
||
|
|
||
|-- OPEN sid=… host=example.com port=443 ------>| dial example.com:443
|
||
|<- OK body=chunk_max --------------------------|
|
||
|
|
||
|-- UPLOAD sid seq=0 payload=TLS ClientHello>| write() to target
|
||
|<- OK ----------------------------------------|
|
||
|-- DOWNLOAD sid seq=0 ack=0 limit=1400 count=4>|
|
||
|<- DATA(1400) DATA(1400) DATA(900) WAIT --------|
|
||
|-- UPLOAD sid seq=517 payload=… ------------->|
|
||
|<- OK ----------------------------------------|
|
||
|-- DOWNLOAD sid seq=3700 ack=3700 … ---------->|
|
||
|<- EOF ----------------------------------------|
|
||
|-- CLOSE sid ---------------------------------->|
|
||
|<- OK ----------------------------------------|
|
||
```
|
||
|
||
Note `ack` trailing behind `seq`: the client advances `ack` only when the
|
||
application has actually read the bytes, which is what applies backpressure all
|
||
the way to the origin server.
|
||
|
||
---
|
||
|
||
## 5. The Go client
|
||
|
||
Source: `core/cmd/dragontcp-client/`.
|
||
|
||
### 5.1 The local proxy front end (`main.go`)
|
||
|
||
The client listens on `127.0.0.1:8080` and speaks ordinary HTTP proxy protocol:
|
||
|
||
* **`CONNECT host:port`** — opens a tunnel, replies `200 Connection
|
||
Established`, then relays bytes in both directions. This is the path used for
|
||
HTTPS and, on Android, for everything.
|
||
* **Plain `GET http://…`** — the request line is rewritten to origin form, the
|
||
`Connection`, `Proxy-Connection`, and `Proxy-Authorization` headers are
|
||
stripped, a `Host` header is synthesised if missing, and `Connection: close`
|
||
is appended.
|
||
|
||
Accepts are bounded by `--max-connections` (default 20 000) using a slot
|
||
channel; over the limit the client returns `503`. Relaying uses `io.Copy` in both
|
||
directions and waits for **both** directions to finish, preserving TCP half-close
|
||
so large or slow responses are not truncated.
|
||
|
||
### 5.2 `chunkConn` — a stream that looks like a socket
|
||
|
||
`openChunkTunnel` returns a `chunkConn` that implements `net.Conn`, so the proxy
|
||
front end does not know it is talking to a record protocol. Internally it keeps:
|
||
|
||
* `upOffset` — bytes sent so far; used as the upload sequence,
|
||
* `downloadOffset` — the next byte the client will ask for,
|
||
* `consumedOffset` — the next byte the application has not yet read; sent as
|
||
`ack`,
|
||
* `readBuf` — data received but not yet handed to the reader,
|
||
* two independent `requestLane`s, one for uploads and one for downloads, so a
|
||
blocking download poll never delays an upload.
|
||
|
||
`Write` slices the caller's buffer into records of the current upload size and
|
||
sends them one at a time, each acknowledged before the next. `Read` refills
|
||
`readBuf` through `fillReadBuffer`, which issues batched download requests.
|
||
|
||
### 5.3 Request lanes and connection reuse
|
||
|
||
A `requestLane` owns at most one physical TCP connection and serialises requests
|
||
onto it with a mutex. `reconnectEvery` controls rotation:
|
||
|
||
| `--chunk-reconnect-every` | Behaviour |
|
||
|---|---|
|
||
| `0` | Persistent. Keep one connection for the life of the lane. |
|
||
| `1` | Auto. The path probe decides: persistent if reuse worked, otherwise one logical request per TCP connection. |
|
||
| `N ≥ 2` | Rotate: close and redial after N logical requests. |
|
||
|
||
Any I/O error discards the connection immediately; the next request redials. All
|
||
sockets get `TCP_NODELAY` and 30-second keepalives, and optionally explicit
|
||
socket buffer sizes via `--tcp-buffer` (0 leaves OS autotuning alone, which is
|
||
the right default).
|
||
|
||
### 5.4 Path probing
|
||
|
||
Before the first real connection, `getPathProfile` measures the path once and
|
||
caches the result for **30 minutes**, keyed by server address, token, and the
|
||
size bounds.
|
||
|
||
Upload and download are probed **independently and concurrently**, each by
|
||
binary search over a fixed ladder of candidate sizes:
|
||
|
||
```text
|
||
32, 64, 128, 256, 512, 1024, 1200, 1280, 1320, 1350, 1360, 1380, 1400,
|
||
1450, 1600, 2048, 3205, 4096, 8192, 16384, 32768, 65536, 98304,
|
||
131072, 262144, 524288, 786432, 1048576
|
||
```
|
||
|
||
The ladder is filtered to `[--chunk-min, --chunk-max]`, and the configured bounds
|
||
are added if missing. Binary search means roughly five probes instead of
|
||
twenty-eight, and — critically — it means the client does not have to *fail* at
|
||
every size on the way down during real traffic.
|
||
|
||
A third probe sends eight keepalives on a single connection to decide whether
|
||
request reuse survives the path. Each probe uses a fresh random session ID and a
|
||
timeout capped at 2.5 s; if the whole search does not finish within 20 s, the
|
||
client falls back to 32 768 up / 1 350 down.
|
||
|
||
The result is logged once:
|
||
|
||
```text
|
||
path probe: upload=32768 download=1400 persistent=true
|
||
```
|
||
|
||
### 5.5 Adaptive record sizing
|
||
|
||
Runtime adaptation remains as a safety net after probing, in `adaptiveSizer`.
|
||
Each direction keeps its own instance plus two landmarks: `good` (largest size
|
||
known to work) and `bad` (smallest size known to fail).
|
||
|
||
**On failure** at the current size:
|
||
|
||
* record `bad = min(bad, attempted)`,
|
||
* drop to `good` if a smaller known-good size exists, otherwise halve,
|
||
* clamp to `--chunk-min`, and force strict decrease.
|
||
|
||
**On success** at the current size, after `--chunk-grow-after` consecutive
|
||
successes (default 16):
|
||
|
||
* if a `bad` landmark is known and is more than one step above, move **halfway
|
||
toward it** — a binary search upward rather than a blind jump,
|
||
* otherwise clear the stale `bad` landmark and grow by `max(current/4, 32)`,
|
||
* clamp to the maximum.
|
||
|
||
If `bad - good ≤ 64` the required success count is multiplied by eight: once the
|
||
working size is bracketed tightly, the controller stops probing the ceiling
|
||
aggressively and settles.
|
||
|
||
Changes are logged:
|
||
|
||
```text
|
||
adaptive upload chunk: 32768 -> 16384 after transport failure
|
||
adaptive download chunk: 1400 -> 700 after transport failure
|
||
adaptive upload chunk: 700 -> 1050 after stable success
|
||
```
|
||
|
||
### 5.6 Download batching and pipeline depth
|
||
|
||
One download request can return many records. The batch size is
|
||
`--chunk-concurrency` (1–256, default 1), additionally capped so that one batch
|
||
carries roughly 1 MiB of useful data:
|
||
|
||
```go
|
||
count = min(pipeline, maxPipeline, (1 MiB) / chunkSize)
|
||
```
|
||
|
||
This matters most on restricted paths. If the safe record size is 32 bytes, one
|
||
TCP/53 request can still return many 32-byte records instead of needing a fresh
|
||
request for every 32 useful bytes.
|
||
|
||
Depth adapts within `1..N`: it starts at the ceiling, **halves** on transport
|
||
failure, and **grows by one** after successful data responses. A ceiling of 1
|
||
disables the mechanism and stays fixed at 1.
|
||
|
||
```text
|
||
adaptive download pipeline: 64 -> 32 after transport failure
|
||
```
|
||
|
||
The escalation order on repeated failure is deliberate: shrink the pipeline
|
||
first, and only when depth is already 1 start shrinking the record size. Eight
|
||
consecutive failures at the minimum record size abort the connection with an
|
||
error rather than spinning forever.
|
||
|
||
---
|
||
|
||
## 6. The Go server
|
||
|
||
Source: `core/cmd/dragontcp-server/`.
|
||
|
||
### 6.1 Connection handling
|
||
|
||
The server accepts on `0.0.0.0:53` by default, bounded by `--max-connections`.
|
||
Each connection runs a loop: read one request (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 any order
|
||
the client chooses.
|
||
|
||
### 6.2 Session state
|
||
|
||
Each `OPEN` creates a `streamSession` holding the real TCP connection to the
|
||
target plus a **download buffer**:
|
||
|
||
* `buf` holds bytes that have arrived from the target but are not yet
|
||
acknowledged by the client,
|
||
* `base` is the absolute stream offset of `buf[0]`,
|
||
* a dedicated goroutine reads the target in 64 KiB chunks and appends to `buf`.
|
||
|
||
That goroutine **blocks when the buffer is full**, which is the whole flow
|
||
control story: a slow phone stops draining, `ack` stops advancing, the buffer
|
||
fills, the server stops reading, and TCP backpressure propagates to the origin
|
||
server. Buffer size is `--chunk-buffered × 65536`, clamped to 1 MiB…64 MiB
|
||
(default 256 → 16 MiB per session).
|
||
|
||
`ack` drops acknowledged bytes off the front and advances `base`. The buffer is
|
||
compacted when its capacity grows past four times its length and exceeds 1 MiB,
|
||
so long-lived sessions do not hold onto peak allocations.
|
||
|
||
### 6.3 Serving a download
|
||
|
||
`readAt(offset, limit, wait)` enforces that `offset` is within
|
||
`[base, base+len(buf)]` — a request below `base` is an error, because those bytes
|
||
were already acknowledged and discarded.
|
||
|
||
Two behaviours are worth knowing:
|
||
|
||
* **Long poll.** The *first* record of a batch waits up to `--chunk-poll-wait`
|
||
(default 200 ms) for data. Later records in the same batch do not wait: the
|
||
batch drains whatever is buffered and then returns `StatusWait`. This keeps
|
||
batches from stalling on partially-filled pipelines.
|
||
* **Coalescing.** If less than `limit` bytes are available, the server waits up
|
||
to 2 ms more for the target to produce more. Without this, a 1-byte read from
|
||
the origin would become a permanent 1-byte tunnel record, and the per-record
|
||
overhead would dominate.
|
||
|
||
### 6.4 Serving an upload
|
||
|
||
Uploads must arrive in exact order: `offset` must equal the session's
|
||
`expectedUp`. Two cases are special-cased:
|
||
|
||
* an offset entirely **below** `expectedUp` is treated as an idempotent retry
|
||
after a lost ACK and silently succeeds,
|
||
* a partially overlapping retry is rejected as an error.
|
||
|
||
This is what makes it safe for the client to resend a record whose response was
|
||
lost when a connection died mid-request.
|
||
|
||
### 6.5 Safety and lifecycle
|
||
|
||
* **Token.** Compared with `crypto/subtle.ConstantTimeCompare` on both `PROBE`
|
||
and `OPEN`. An empty token means no authentication.
|
||
* **Target filtering.** By default the server refuses to dial unspecified,
|
||
multicast, private, loopback, link-local, and a list of special-use prefixes
|
||
(`0.0.0.0/8`, `100.64.0.0/10`, `192.0.2.0/24`, `198.18.0.0/15`, `240.0.0.0/4`,
|
||
`2001:db8::/32`, and others). `--allow-private` disables this. **Leave it off
|
||
on a public server** — it is what stops the tunnel being used to reach your
|
||
VPS's own localhost services and cloud metadata endpoints.
|
||
* **DNS cache.** Bounded map with a TTL (`--dns-cache-ttl`, default 30 s;
|
||
`--dns-cache-size`, default 4096). When full it resets wholesale rather than
|
||
evicting entry by entry — cheap, and adequate for a hot cache.
|
||
* **Idle reaping.** A sweep every 30 s closes sessions idle longer than
|
||
`--chunk-session-timeout` (default 2 minutes).
|
||
* **Debug.** `--debug` logs accepts, session opens, and errors to stderr, and
|
||
`--debug-stats-interval` prints counters (bytes up/down, push records, pull
|
||
requests, data/wait records, active sessions). `--debug-chunks` logs every
|
||
record and is very verbose.
|
||
|
||
---
|
||
|
||
## 7. The Android app
|
||
|
||
Package `com.dragontcp.client`, `minSdk 29`, `targetSdk 29`, arm64 only.
|
||
|
||
### 7.1 Process model
|
||
|
||
The APK ships the Go client at `lib/arm64-v8a/libdragontcp_client.so`. Despite
|
||
the name it is not a shared library — it is a **statically linked Go
|
||
executable**. The `lib*.so` naming and `extractNativeLibs="true"` make Android
|
||
unpack it into `nativeLibraryDir` with the executable bit set, which is the
|
||
standard way to ship a helper binary in an APK without needing an installer.
|
||
|
||
`DragonService` launches it with `ProcessBuilder`, merges stderr into stdout,
|
||
and reads its output on a background thread. Only interesting lines reach the
|
||
UI log: those starting 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.
|
||
|
||
### 7.2 Startup sequence
|
||
|
||
1. `MainActivity` validates the form and saves it to `SharedPreferences`.
|
||
2. `VpnService.prepare()` — the system consent dialog, if not already granted.
|
||
3. `DragonService` starts in the foreground with a notification carrying a STOP
|
||
action.
|
||
4. The Go core is spawned with flags built from the saved settings.
|
||
5. The service polls `127.0.0.1:8080` for up to 10 s until the proxy accepts.
|
||
6. The TUN interface is established.
|
||
7. `TunnelEngine` starts and the state broadcasts flip to `CONNECTED`.
|
||
|
||
A service-side guard makes a duplicate CONNECT intent a no-op, so a double tap
|
||
cannot tear down a healthy tunnel and restart it.
|
||
|
||
### 7.3 The TUN interface
|
||
|
||
```java
|
||
setSession("DragonTCP Lite")
|
||
setMtu(1400)
|
||
addAddress("10.77.0.2", 32) addRoute("0.0.0.0", 0)
|
||
addAddress("fd77:6472:6167:6f6e::2", 128) addRoute("::", 0)
|
||
addDnsServer("1.1.1.1")
|
||
addDisallowedApplication(<self>)
|
||
setBlocking(true) setMetered(false)
|
||
```
|
||
|
||
Two decisions matter here:
|
||
|
||
* **IPv6 is captured, then dropped.** The userspace stack is IPv4-only. Routing
|
||
`::/0` into the tunnel and discarding it is what prevents apps from quietly
|
||
bypassing the tunnel over IPv6. It is a blackhole by design, not an oversight.
|
||
* **The app excludes itself** from the VPN, and every upstream socket is
|
||
additionally passed through `VpnService.protect()`. Both are needed so the Go
|
||
core's connection to your VPS does not get routed back into the TUN it is
|
||
serving.
|
||
|
||
### 7.4 The userspace TCP/IP stack
|
||
|
||
`TunnelEngine` (Kotlin, adapted from FreeProxy — see §16) runs one reader thread
|
||
and one writer thread over the TUN file descriptor, plus a cached thread pool
|
||
exposed to coroutines for per-session blocking I/O.
|
||
|
||
* IPv4 packets are parsed; anything else (IPv6, fragments, ICMP) is dropped.
|
||
* **TCP** goes to a `TcpSession` keyed by the 4-tuple. The session acts as the
|
||
server endpoint towards the phone's own kernel: it answers SYN with SYN-ACK,
|
||
acknowledges data, and sends FIN/RST. Because the "link" to the kernel is
|
||
lossless, there is no congestion control — it only has to respect the peer's
|
||
advertised receive window. New flows are only created by a SYN; anything else
|
||
gets an RST so apps fail fast instead of hanging. Limit: 512 concurrent TCP
|
||
sessions.
|
||
* **UDP** goes to a `UdpSession`. With an HTTP CONNECT upstream, general UDP
|
||
cannot be carried, so **only DNS is handled**: queries are converted to
|
||
DNS-over-TCP (RFC 7766, 2-byte length prefix) and sent to **1.1.1.1:53**
|
||
through the tunnel. All other UDP is dropped, which makes QUIC fail and pushes
|
||
apps back to TCP. Limit: 256 sessions.
|
||
* Housekeeping every 5 s expires idle sessions (TCP 300 s, DNS 20 s, other UDP
|
||
120 s). The TUN write queue holds 1024 packets and drops on overflow rather
|
||
than blocking session threads.
|
||
|
||
Real traffic reaches the Go proxy through `ProxyClient`, which opens a protected
|
||
socket to `127.0.0.1:8080` and issues `CONNECT <ip>:<port>` per stream.
|
||
|
||
### 7.5 Settings and how they map to flags
|
||
|
||
| UI field | Default | Flag passed to the core |
|
||
|---|---|---|
|
||
| Server | — | `--server-host` |
|
||
| Port | 53 | `--server-port` |
|
||
| Token | empty | `--token` (omitted entirely when blank) |
|
||
| Max chunk | 1048576 | `--chunk-max` **and** `--chunk-start` |
|
||
| Min chunk | 32 | `--chunk-min` |
|
||
| Concurrency | 1 | `--chunk-concurrency` |
|
||
| Reconnect every | 0 | `--chunk-reconnect-every` |
|
||
| Timeout (s) | 2 | `--chunk-timeout` |
|
||
|
||
Fixed by the service: `--listen-host 127.0.0.1`, `--listen-port 8080`,
|
||
`--transport chunk`, `--chunk-pollers 1`, `--chunk-grow-after 16`,
|
||
`--chunk-adapt-log=true`.
|
||
|
||
In the current source, `Reconnect every` accepts `0` and `0` means persistent.
|
||
(Older *prebuilt* APKs shipped a Logs-page UI whose Reconnect field required at
|
||
least `1`; on those, `1` selects Auto. If you build from this source you get the
|
||
explicit `0 = persistent` behaviour.)
|
||
|
||
`AppLog` keeps the last 600 lines in memory and pushes them live to
|
||
`LogActivity`. It is not persisted to disk.
|
||
|
||
---
|
||
|
||
## 8. Building on Windows
|
||
|
||
No Gradle and no Android Studio required. `android\build_apk.ps1` drives the
|
||
Android SDK command-line tools directly.
|
||
|
||
### 8.1 Quick start
|
||
|
||
```powershell
|
||
cd android
|
||
.\build_apk.ps1
|
||
```
|
||
|
||
Or double-click `android\build_apk.cmd`. The result is:
|
||
|
||
```text
|
||
android\build\DragonTCP-Hybrid-arm64.apk
|
||
```
|
||
|
||
signed with a debug keystore that is generated on first run.
|
||
|
||
### 8.2 Requirements
|
||
|
||
| Component | How it is found | Needed? |
|
||
|---|---|---|
|
||
| **Android SDK** | `ANDROID_SDK_ROOT`, `ANDROID_HOME`, `%LOCALAPPDATA%\Android\Sdk`, `C:\Android\Sdk`, or `-SdkRoot` | Yes |
|
||
| **build-tools** | Newest installed version that has `aapt.exe`, `d8.bat`, `apksigner.bat`, `zipalign.exe`; or `-BuildTools` | Yes |
|
||
| **Platform** | `android-35` if present, else the newest with an `android.jar`; or `-Platform` | Yes |
|
||
| **JDK 17+** | `JAVA_HOME`, then `javac` on `PATH` (only if `jar.exe` sits beside it), then `C:\Program Files\Java\*`; or `-JavaHome` | Yes — a JRE is not enough |
|
||
| **Kotlin** | `KOTLIN_HOME`, else `android\.tools\kotlinc-<version>`; downloaded automatically if absent | Auto |
|
||
| **Go** | `PATH`, or `-GoBin` on `build_core.ps1` | Only to rebuild the `.so` |
|
||
|
||
If the Kotlin compiler is missing, the script downloads it once (~85 MB) from
|
||
the JetBrains GitHub releases into `android\.tools\` and reuses it forever after.
|
||
Pass `-NoDownload` to make a missing Kotlin a hard error instead.
|
||
|
||
### 8.3 What the script actually does
|
||
|
||
1. **Resolve the toolchain** and print what it picked.
|
||
2. **Build the native core** if `lib\arm64-v8a\libdragontcp_client.so` is missing
|
||
(or `-BuildCore` was passed) by calling `..\build_core.ps1 -ClientOnly`.
|
||
3. **`aapt package`** — compile `res/`, pack `assets/`, bind the manifest against
|
||
`android.jar`, producing `resources.ap_`.
|
||
4. **Kotlin** — compile every `.kt` under `src\` to `build\kclasses`, targeting
|
||
JVM 1.8, against `android.jar` + coroutines + stdlib.
|
||
5. **Java** — compile every `.java` under `src\` to `build\jclasses` with
|
||
`--release 8`, against `android.jar` + the Kotlin output + stdlib.
|
||
6. **`jar`** both class trees, then **`d8`** them together with
|
||
`kotlin-stdlib`, `kotlin-stdlib-jdk7/8`, and `kotlinx-coroutines-core-jvm`
|
||
into `classes.dex` at `--min-api 29`.
|
||
7. **Package** — copy `resources.ap_` to the APK and add `classes*.dex` plus the
|
||
whole `lib\` tree using .NET's `ZipArchive` (Windows has no `zip` command).
|
||
8. **`zipalign -p -f 4`**, then **`apksigner sign`**, then
|
||
**`apksigner verify --verbose`**.
|
||
|
||
### 8.4 Options
|
||
|
||
```powershell
|
||
.\build_apk.ps1 -BuildCore # rebuild the Go .so first
|
||
.\build_apk.ps1 -BuildTools 35.0.0 -Platform android-35
|
||
.\build_apk.ps1 -JavaHome 'C:\Program Files\Java\jdk-21.0.10'
|
||
.\build_apk.ps1 -KotlinHome C:\kotlinc -NoDownload
|
||
.\build_apk.ps1 -KotlinVersion 2.2.0
|
||
.\build_apk.ps1 -Keystore C:\keys\release.jks -KsPass … -KeyAlias … -KeyPass …
|
||
```
|
||
|
||
Go binaries alone:
|
||
|
||
```powershell
|
||
.\build_core.ps1 # android client .so + linux amd64/arm64 servers
|
||
.\build_core.ps1 -ClientOnly # just the .so
|
||
```
|
||
|
||
### 8.5 Windows-specific notes
|
||
|
||
Three things differ from the shell build and are worth knowing before editing the
|
||
script:
|
||
|
||
* **The Kotlin compiler is invoked as
|
||
`java -cp kotlin-compiler.jar org.jetbrains.kotlin.cli.jvm.K2JVMCompiler`, not
|
||
through `kotlinc.bat`.** `cmd.exe` treats `;` as an argument separator, so a
|
||
`-classpath a.jar;b.jar` handed to a batch file is split into two arguments and
|
||
the second jar is misread as a source file. Calling `java.exe` directly avoids
|
||
the batch tokenizer entirely.
|
||
* **`d8.bat` and `apksigner.bat` are still batch files.** Their arguments contain
|
||
no semicolons today, so they work — but a project path containing spaces or
|
||
semicolons could hit the same class of problem.
|
||
* **`zipalign -p -f 4` runs before signing.** The shell script omits it; it is
|
||
the canonical ordering and costs nothing.
|
||
|
||
`android\.gitignore` keeps `build/`, `.tools/`, and the debug keystore out of
|
||
version control.
|
||
|
||
---
|
||
|
||
## 9. Building on Linux and macOS
|
||
|
||
```bash
|
||
./build_core.sh # Go: android .so + linux amd64/arm64 servers
|
||
./android/build_apk.sh
|
||
./build_all.sh # both
|
||
```
|
||
|
||
`build_apk.sh` expects `ANDROID_SDK_ROOT` (or `ANDROID_HOME`) and a `KOTLIN_HOME`
|
||
pointing at a Kotlin distribution that bundles
|
||
`lib/kotlinx-coroutines-core-jvm.jar` — it defaults to
|
||
`~/.sdkman/candidates/kotlin/current`. Unlike the Windows script it does not
|
||
download anything for you.
|
||
|
||
Go builds by hand, if you prefer:
|
||
|
||
```bash
|
||
cd core
|
||
go test ./...
|
||
|
||
GOOS=android GOARCH=arm64 CGO_ENABLED=0 \
|
||
go build -trimpath -ldflags='-s -w' \
|
||
-o ../android/lib/arm64-v8a/libdragontcp_client.so ./cmd/dragontcp-client
|
||
|
||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||
go build -trimpath -ldflags='-s -w' \
|
||
-o ../bin/dragontcp-hybrid-server-linux-amd64 ./cmd/dragontcp-server
|
||
```
|
||
|
||
CGO is off everywhere, so no NDK and no C toolchain are required for any target.
|
||
|
||
---
|
||
|
||
## 10. Running the server
|
||
|
||
```bash
|
||
sudo ./dragontcp-hybrid-server-linux-amd64 --port 53 --chunk-max 1048576
|
||
```
|
||
|
||
With a token:
|
||
|
||
```bash
|
||
sudo ./dragontcp-hybrid-server-linux-amd64 \
|
||
--token 'YOUR_SECRET' --port 53 --chunk-max 1048576
|
||
```
|
||
|
||
With diagnostics:
|
||
|
||
```bash
|
||
sudo ./dragontcp-hybrid-server-linux-amd64 \
|
||
--port 53 --chunk-max 1048576 --debug --debug-stats-interval 10s
|
||
```
|
||
|
||
`sudo` is only needed because port 53 is privileged. If `systemd-resolved` or
|
||
`dnsmasq` already owns port 53, free it first or pick another port. The server
|
||
creates no TUN device and needs no NAT or `iptables` rules.
|
||
|
||
| Flag | Default | Meaning |
|
||
|---|---|---|
|
||
| `--host` | `0.0.0.0` | Listen address |
|
||
| `--port` | `53` | Listen port |
|
||
| `--token` | empty | Optional shared secret |
|
||
| `--max-connections` | `20000` | Concurrent TCP connections |
|
||
| `--allow-private` | `false` | Allow private/loopback targets — **keep off in public** |
|
||
| `--dns-cache-ttl` | `30s` | Resolver cache lifetime |
|
||
| `--dns-cache-size` | `4096` | Cached hostnames |
|
||
| `--tcp-buffer` | `0` | Explicit socket buffers; 0 = OS autotuning |
|
||
| `--chunk-max` | `1048576` | Largest record the server accepts (32 B – 1 MiB) |
|
||
| `--chunk-buffered` | `256` | Per-session buffer in 64 KiB units (≈16 MiB) |
|
||
| `--chunk-poll-wait` | `200ms` | Long-poll wait for the first record of a batch |
|
||
| `--chunk-session-timeout` | `2m` | Idle session reaping |
|
||
| `--debug` | `false` | Session/connect/error logs plus periodic stats |
|
||
| `--debug-chunks` | `false` | Log every record — very verbose |
|
||
| `--debug-stats-interval` | `5s` | Statistics period; 0 disables |
|
||
|
||
---
|
||
|
||
## 11. Running the client
|
||
|
||
### On Android
|
||
|
||
Install the APK, enter the server IP and port, grant the VPN prompt, connect.
|
||
Recommended starting point:
|
||
|
||
```text
|
||
Server: YOUR_SERVER_IP
|
||
Port: 53
|
||
Token: (match the server, or leave blank)
|
||
Max chunk: 1048576
|
||
Min chunk: 32
|
||
Concurrency: 1
|
||
Reconnect every: 0
|
||
Timeout (s): 2
|
||
```
|
||
|
||
Use **OPEN LOGS** to watch the path probe and any adaptation.
|
||
|
||
### As a CLI
|
||
|
||
```bash
|
||
./dragontcp-hybrid-client-linux-amd64 \
|
||
--server-host YOUR_SERVER_IP --server-port 53 \
|
||
--listen-port 8080 --chunk-max 1048576
|
||
```
|
||
|
||
Then point anything at `http://127.0.0.1:8080` as an HTTP proxy.
|
||
|
||
| Flag | Default | Meaning |
|
||
|---|---|---|
|
||
| `--listen-host` / `--listen-port` | `127.0.0.1` / `8080` | Local proxy bind |
|
||
| `--server-host` / `--server-port` | — / `53` | Remote server (host required) |
|
||
| `--token` | empty | Shared secret |
|
||
| `--transport` | `chunk` | Must be `chunk` |
|
||
| `--max-connections` | `20000` | Concurrent proxied connections |
|
||
| `--tcp-buffer` | `0` | Explicit socket buffers |
|
||
| `--chunk-start` | `1048576` | Initial record size (probe overrides it) |
|
||
| `--chunk-min` | `32` | Floor |
|
||
| `--chunk-max` | `1048576` | Ceiling, further clamped by the server |
|
||
| `--chunk-adaptive` | `true` | Enable runtime resizing |
|
||
| `--chunk-grow-after` | `16` | Successes before growing |
|
||
| `--chunk-adapt-log` | `true` | Print size changes |
|
||
| `--chunk-size` | `0` | Legacy: pins start/min/max and disables adaptation |
|
||
| `--chunk-concurrency` | `1` | Download batch ceiling, 1–256 |
|
||
| `--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-pollers` | `1` | Reserved compatibility knob; unused |
|
||
|
||
---
|
||
|
||
## 12. Tuning guide
|
||
|
||
**Start with the defaults.** Path probing already picks sensible sizes; most
|
||
manual tuning makes things worse.
|
||
|
||
* **Throughput feels capped.** Raise `Concurrency` to 4–16. More records per
|
||
request is the main lever when latency to the server is high, because each
|
||
round trip returns more data. Watch the logs — if you see repeated
|
||
`adaptive download pipeline: N -> N/2`, the path cannot sustain that depth.
|
||
* **Frequent `after transport failure` lines.** The network is dropping large
|
||
records. Lower `Max chunk` to something the probe already found safe (1400 is
|
||
common) so the client stops rediscovering the limit.
|
||
* **Connection dies after a while, then recovers.** A middlebox is capping
|
||
requests per connection. Set `Reconnect every` to something like 8–32.
|
||
* **Nothing connects at all, but the probe succeeds.** Check the token matches,
|
||
and check that the server is not refusing the target because it resolves to a
|
||
private address.
|
||
* **High latency, low bandwidth link.** Leave `--tcp-buffer` at 0 first. Only if
|
||
you have a small number of high-BDP connections is `1048576` or `4194304`
|
||
worth trying; on many connections it costs memory for nothing.
|
||
* **Server memory.** Each session can hold up to `--chunk-buffered × 64 KiB`
|
||
(default 16 MiB). With many concurrent sessions, lower it.
|
||
|
||
---
|
||
|
||
## 13. Troubleshooting
|
||
|
||
### Build
|
||
|
||
| Symptom | Cause and fix |
|
||
|---|---|
|
||
| `Android SDK not found` | Set `ANDROID_SDK_ROOT` or pass `-SdkRoot 'D:\Android\Sdk'`. |
|
||
| `No usable build-tools found` | Install build-tools via the SDK Manager; the script needs `aapt`, `d8`, `apksigner`, `zipalign` together. |
|
||
| `Missing …\jar.exe (a JRE is not enough)` | You have a JRE or the `javapath` shim. Install a JDK and set `JAVA_HOME`. |
|
||
| `Kotlin download failed` | No network, or a proxy. Download `kotlin-compiler-<ver>.zip` by hand, extract it, and pass `-KotlinHome <dir>\kotlinc`. |
|
||
| `source entry is not a Kotlin file: …jar` | You reintroduced `kotlinc.bat`. See §8.5 — call the compiler jar through `java.exe`. |
|
||
| `libdragontcp_client.so is missing` | Install Go and run `.\build_core.ps1 -ClientOnly`, or pass `-BuildCore`. |
|
||
| `run ..\build_core.ps1 first` on Linux | Use `./build_core.sh`; the shell script does not build the core for you. |
|
||
|
||
### Runtime
|
||
|
||
| Symptom | Cause and fix |
|
||
|---|---|
|
||
| `CONNECT failed: Server is required` | Empty server field. |
|
||
| `Local proxy did not start` | The Go core died within 10 s. Open the logs; usually a bad flag or an unusable port. |
|
||
| `DragonTCP core exited: N` | The core process died while connected. The whole tunnel is torn down deliberately. |
|
||
| `authentication failed` | Token mismatch between app and server. |
|
||
| `target resolves only to blocked addresses` | The destination is private/loopback. Intentional; `--allow-private` on the server overrides it, at real risk. |
|
||
| `download offset N was already acknowledged` | Client and server disagree on stream position — almost always a stale session after a restart. Reconnect. |
|
||
| `upload gap: got N expected M` | Same, in the upload direction. Reconnect. |
|
||
| DNS works, QUIC/UDP apps do not | By design: only DNS is carried over UDP. Apps fall back to TCP. |
|
||
| No IPv6 anywhere | By design: IPv6 is captured and blackholed to prevent bypass. |
|
||
|
||
---
|
||
|
||
## 14. Version history
|
||
|
||
**Hybrid v1** — the current wire protocol.
|
||
|
||
* Kept the lightweight Android TUN → local HTTP proxy architecture.
|
||
* Replaced ASCII `UP`/`OK`/`CPUSH`/`CPULL` framing with compact binary records.
|
||
* Replaced the fixed `0xAD` XOR with a changing SHA-256-derived keystream.
|
||
* Added automatic upload/download path-size probing with binary search.
|
||
* Kept separate adaptive sizes per direction.
|
||
* Added download batching with adaptive pipeline depth.
|
||
* Records up to 1 MiB; token optional; TCP/53 default.
|
||
* Reconnect selectable: persistent, auto, or forced rotation.
|
||
|
||
**Hybrid v2** — withdrawn. Introduced a multi-request upload pipeline and
|
||
65 535-record mega-batches; both proved unreliable. None of it is present here.
|
||
|
||
**Hybrid v3 "SafeSpeed"** — built directly from the confirmed-working v1.
|
||
|
||
* Wire encoding byte-for-byte unchanged from v1: same headers, same 16-byte
|
||
session IDs, same keystream, same probes, same upload transactions, same
|
||
server framing.
|
||
* Download concurrency became user-configurable, 1–256, default 1.
|
||
* `1` keeps the pipeline fixed at one. Above 1, depth starts at the ceiling,
|
||
halves on transport failure, and grows by one on success, staying in `1..N`.
|
||
* Server cap remains 256 records per batch.
|
||
* Upload remains one framed request followed by one response ACK.
|
||
|
||
---
|
||
|
||
## 15. Testing and validation
|
||
|
||
`cd core && go test ./...` covers:
|
||
|
||
* the masking round-trip, and that different sequences produce different wire
|
||
bytes,
|
||
* the adaptive sizer recovering from the minimum rather than latching there,
|
||
* `reconnectEvery == 0` meaning persistent.
|
||
|
||
Current status on this checkout:
|
||
|
||
```text
|
||
ok dragontcp/cmd/dragontcp-client
|
||
ok dragontcp/cmd/dragontcp-server
|
||
? dragontcp/internal/protocol [no test files]
|
||
ok dragontcp/internal/wire
|
||
```
|
||
|
||
Beyond unit tests, the transport was exercised with:
|
||
|
||
* an 8 MiB HTTP download through the proxy, verified by SHA-256,
|
||
* an HTTPS `CONNECT` download verified byte for byte,
|
||
* persistent connection mode,
|
||
* forced `reconnect-every-1` mode,
|
||
* a server restricted to 1400-byte records, where probing selected 1400
|
||
automatically and the download still completed correctly.
|
||
|
||
`SHA256SUMS` records digests for the published binaries.
|
||
|
||
---
|
||
|
||
## 16. Licensing
|
||
|
||
The Android userspace TCP/IP stack under
|
||
`android/src/tech/xvanturing/freeproxy/` is adapted from **FreeProxy** by
|
||
xVanTuring, licensed under the **Apache License 2.0**. Modified files carry a
|
||
marker comment at the top.
|
||
|
||
* Full attribution: [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)
|
||
* Full license text: [`licenses/FreeProxy-APACHE-2.0.txt`](licenses/FreeProxy-APACHE-2.0.txt)
|
||
* Both are also shipped inside the APK under `assets/`.
|
||
|
||
Those notice files are a license condition, not documentation — they are kept
|
||
separate from this README deliberately, and should not be folded into it or
|
||
deleted.
|
||
|
||
The remaining DragonTCP glue, UI, Go transport, and server code is provided as
|
||
part of this project.
|