V14
This commit is contained in:
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
bff5e7d9bd9b133ceb90987ad5914f1f26d085ad699cd7d0fc7341e9f713c52f bin/dragontcp-hybrid-server-linux-amd64
|
4e4a16f6c537f2c6be5c104f6e50ea907122092a21402a70506fe12e6f144bb7 bin/dragontcp-hybrid-server-linux-amd64
|
||||||
333718a76ce4a89d968cd7fb4aa74f0365a861a3f0ac36fe933085ca5d67fa11 bin/dragontcp-hybrid-server-linux-arm64
|
39a6bb16cbddefb50e791220747f5cd01b0c894992c657b24c6c2e5d1501299f bin/dragontcp-hybrid-server-linux-arm64
|
||||||
94f3fb7cf895b79f0db355ac51b8f11cd14a7d8641e552dfb1b31aad326666ff bin/dragontcp-hybrid-client-linux-amd64
|
66ad3741d84b4e73098912725af762824bec629adab5522a5e161b4e23b79dee bin/dragontcp-hybrid-client-linux-amd64
|
||||||
ed361cf7a6d72b1c22a111875957e5111f3088bc2843fff02361eb2bc2dc967b android/lib/arm64-v8a/libdragontcp_client.so
|
fa6b41cc8c2999932cc78f731cb249cab677af9df7701c491fb688f9cbcb089d android/lib/arm64-v8a/libdragontcp_client.so
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Build outputs and the auto-downloaded Kotlin compiler (~85 MB).
|
||||||
|
/build/
|
||||||
|
/.tools/
|
||||||
|
/dragontcp-lite-debug.jks
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.dragontcp.client"
|
package="com.dragontcp.client"
|
||||||
android:versionCode="12"
|
android:versionCode="13"
|
||||||
android:versionName="12.0-hybrid">
|
android:versionName="13.0-hybrid">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
|
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
# DragonTCP Hybrid Transport
|
|
||||||
|
|
||||||
This branch keeps the lightweight DragonTCP Android architecture that was
|
|
||||||
working well:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Android apps
|
|
||||||
-> Android VpnService userspace TCP adapter
|
|
||||||
-> local HTTP/HTTPS CONNECT proxy on 127.0.0.1:8080
|
|
||||||
-> DragonTCP transport over TCP/53
|
|
||||||
-> DragonTCP server
|
|
||||||
-> destination website
|
|
||||||
```
|
|
||||||
|
|
||||||
The heavy raw-IP DragonTCP VPN server is **not** used. The Linux server is
|
|
||||||
again only a relay/proxy server; it does not create a TUN interface and does
|
|
||||||
not need NAT/iptables.
|
|
||||||
|
|
||||||
## What changed on the wire
|
|
||||||
|
|
||||||
The old DragonTCP transport used a fixed text/frame pattern:
|
|
||||||
|
|
||||||
```text
|
|
||||||
UP + request id + length + XOR(0xAD, ASCII CPUSH/CPULL/...)
|
|
||||||
OK + request id + length + XOR(0xAD, response)
|
|
||||||
```
|
|
||||||
|
|
||||||
The Hybrid transport replaces that network layer with compact binary records:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Request header (29 bytes)
|
|
||||||
1 byte mode
|
|
||||||
16 bytes random session ID
|
|
||||||
8 bytes sequence / stream offset
|
|
||||||
4 bytes payload length
|
|
||||||
|
|
||||||
Response header (5 bytes)
|
|
||||||
1 byte status
|
|
||||||
4 bytes body length
|
|
||||||
```
|
|
||||||
|
|
||||||
There are no constant `UP`, `OK`, `CPUSH`, `CPULL`, or `DATA` strings on the
|
|
||||||
wire.
|
|
||||||
|
|
||||||
Payload bytes are masked with a sequence-dependent SHA-256 counter keystream.
|
|
||||||
The operation is still XOR-based, but the XOR bytes change with session,
|
|
||||||
mode, sequence, direction, and block number instead of using the same 0xAD
|
|
||||||
byte for every position.
|
|
||||||
|
|
||||||
This is traffic shaping/obfuscation, not authenticated encryption.
|
|
||||||
|
|
||||||
## Path probing
|
|
||||||
|
|
||||||
Before the first proxied connection, the client probes the phone-to-server
|
|
||||||
path once and caches the result for 30 minutes.
|
|
||||||
|
|
||||||
Upload and download are tested independently against a ladder from small
|
|
||||||
records through 1 MiB. A binary search is used, so it does not need to fail at
|
|
||||||
every size between the maximum and minimum.
|
|
||||||
|
|
||||||
Example log:
|
|
||||||
|
|
||||||
```text
|
|
||||||
path probe: upload=32768 download=1400 persistent=true
|
|
||||||
```
|
|
||||||
|
|
||||||
If the server is capped at 1400 bytes, the client can discover:
|
|
||||||
|
|
||||||
```text
|
|
||||||
path probe: upload=1400 download=1400 persistent=true
|
|
||||||
```
|
|
||||||
|
|
||||||
instead of beginning at 1 MiB and repeatedly halving during real traffic.
|
|
||||||
|
|
||||||
## Separate upload/download sizing
|
|
||||||
|
|
||||||
Upload and download maintain independent record sizes. Runtime adaptation is
|
|
||||||
still present as a safety net if network conditions change after the initial
|
|
||||||
probe.
|
|
||||||
|
|
||||||
Useful log events remain concise:
|
|
||||||
|
|
||||||
```text
|
|
||||||
adaptive upload chunk: 32768 -> 16384 after transport failure
|
|
||||||
adaptive download pipeline: 64 -> 32 after transport failure
|
|
||||||
adaptive download chunk: 1400 -> 700 after transport failure
|
|
||||||
```
|
|
||||||
|
|
||||||
## Batched downloads
|
|
||||||
|
|
||||||
One download request can receive many ordered DATA responses. The client uses
|
|
||||||
one download worker but pipelines up to 256 response records per request,
|
|
||||||
subject to an approximately 1 MiB useful-data cap per batch.
|
|
||||||
|
|
||||||
This is especially important on restricted paths. If the safe record size is
|
|
||||||
32 bytes, one TCP/53 request can still bring back many 32-byte records instead
|
|
||||||
of requiring a new request for every 32 useful bytes.
|
|
||||||
|
|
||||||
## Reconnect behavior
|
|
||||||
|
|
||||||
The transport supports persistent TCP connections and forced connection
|
|
||||||
rotation.
|
|
||||||
|
|
||||||
Core/CLI semantics:
|
|
||||||
|
|
||||||
```text
|
|
||||||
--chunk-reconnect-every 0 persistent
|
|
||||||
--chunk-reconnect-every 1 automatic compatibility mode
|
|
||||||
--chunk-reconnect-every N rotate after N logical requests, N >= 2
|
|
||||||
```
|
|
||||||
|
|
||||||
Automatic mode probes persistent request reuse. If it works, DragonTCP keeps
|
|
||||||
the connection. If it does not, DragonTCP falls back to one logical request
|
|
||||||
per TCP connection.
|
|
||||||
|
|
||||||
The supplied APK is based on the current Logs Page UI, whose Reconnect field
|
|
||||||
still requires a value of at least 1. In this APK, leave it at `1` for Auto.
|
|
||||||
The full Android source in this package also contains the newer explicit
|
|
||||||
`0 = persistent` UI behavior for rebuilding with the Android SDK.
|
|
||||||
|
|
||||||
## Optional token
|
|
||||||
|
|
||||||
The token remains optional.
|
|
||||||
|
|
||||||
Server without token:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./dragontcp-hybrid-server-linux-amd64 --chunk-max 1048576
|
|
||||||
```
|
|
||||||
|
|
||||||
Server with token:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./dragontcp-hybrid-server-linux-amd64 \
|
|
||||||
--token 'YOUR_SECRET' \
|
|
||||||
--chunk-max 1048576
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server
|
|
||||||
|
|
||||||
Default server port is TCP/53:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./dragontcp-hybrid-server-linux-amd64 \
|
|
||||||
--port 53 \
|
|
||||||
--chunk-max 1048576
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful debug mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./dragontcp-hybrid-server-linux-amd64 \
|
|
||||||
--port 53 \
|
|
||||||
--chunk-max 1048576 \
|
|
||||||
--debug \
|
|
||||||
--debug-stats-interval 10s
|
|
||||||
```
|
|
||||||
|
|
||||||
The server is a lightweight TCP relay. It does not create Linux TUN devices.
|
|
||||||
|
|
||||||
## Android
|
|
||||||
|
|
||||||
Install `DragonTCP-Hybrid-Android-arm64.apk`, configure the server IP/port,
|
|
||||||
and connect as before.
|
|
||||||
|
|
||||||
Recommended initial settings:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Server: YOUR_SERVER_IP
|
|
||||||
Port: 53
|
|
||||||
Token: optional
|
|
||||||
Max chunk: 1048576
|
|
||||||
Min chunk: 32
|
|
||||||
Reconnect: 1 (Auto in supplied APK)
|
|
||||||
Timeout: 2
|
|
||||||
```
|
|
||||||
|
|
||||||
The Android TUN frontend continues to point applications at the local
|
|
||||||
DragonTCP HTTP proxy. DNS continues to use the existing lightweight adapter's
|
|
||||||
DNS-over-proxy path.
|
|
||||||
|
|
||||||
## Source layout
|
|
||||||
|
|
||||||
```text
|
|
||||||
core/
|
|
||||||
cmd/dragontcp-client/
|
|
||||||
cmd/dragontcp-server/
|
|
||||||
internal/protocol/ # TCP tuning + legacy helpers
|
|
||||||
internal/wire/ # new compact binary framing + changing mask
|
|
||||||
|
|
||||||
android/
|
|
||||||
src/com/dragontcp/client/
|
|
||||||
src/tech/xvanturing/freeproxy/
|
|
||||||
lib/arm64-v8a/libdragontcp_client.so
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building the Go core
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd core
|
|
||||||
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
go build -trimpath -ldflags='-s -w' \
|
|
||||||
-o ../bin/dragontcp-hybrid-server-linux-amd64 \
|
|
||||||
./cmd/dragontcp-server
|
|
||||||
|
|
||||||
GOOS=android GOARCH=arm64 CGO_ENABLED=0 \
|
|
||||||
go build -trimpath -ldflags='-s -w' \
|
|
||||||
-o ../android/lib/arm64-v8a/libdragontcp_client.so \
|
|
||||||
./cmd/dragontcp-client
|
|
||||||
```
|
|
||||||
|
|
||||||
`android/build_apk.sh` contains the source build procedure for the Android
|
|
||||||
application when an Android SDK and Kotlin compiler are installed.
|
|
||||||
|
|
||||||
## Validation performed
|
|
||||||
|
|
||||||
The new Go transport was tested with:
|
|
||||||
|
|
||||||
- Go unit tests for client/server/wire code.
|
|
||||||
- changing-mask round-trip test.
|
|
||||||
- 8 MiB HTTP download through the DragonTCP proxy with SHA-256 equality.
|
|
||||||
- HTTPS CONNECT download through DragonTCP with byte-for-byte equality.
|
|
||||||
- persistent connection mode.
|
|
||||||
- forced reconnect-every-1 mode.
|
|
||||||
- a server restricted to 1400-byte records, where path probing selected 1400
|
|
||||||
bytes automatically and the download still completed correctly.
|
|
||||||
|
|
||||||
The supplied APK embeds the exact Android ARM64 Go core built from this source.
|
|
||||||
|
|
||||||
|
|
||||||
## SafeSpeed v3
|
|
||||||
|
|
||||||
This release is built directly from the confirmed-working Hybrid v1. The wire protocol is unchanged: same compact binary request/response headers, same 16-byte session IDs, same SHA-256-derived XOR keystream, same probe packets, same upload transaction behavior, and same server framing.
|
|
||||||
|
|
||||||
The only transport scheduling change is that the download pipeline starts at 256 records (the same maximum already supported by v1) instead of starting at 32 and slowly growing one record per successful request. This is especially important when the probed record size is small.
|
|
||||||
|
|
||||||
The broken Hybrid v2 changes are intentionally absent: no multi-request upload pipeline and no 65,535-record mega-batches.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# DragonTCP Hybrid v1
|
|
||||||
|
|
||||||
- Keeps the lightweight Android TUN -> local HTTP proxy architecture.
|
|
||||||
- Replaces ASCII UP/OK/CPUSH/CPULL framing with compact binary records.
|
|
||||||
- Replaces fixed XOR 0xAD payload masking with a changing SHA-256-derived XOR stream.
|
|
||||||
- Adds automatic upload/download path-size probing.
|
|
||||||
- Keeps separate upload and download adaptive sizes.
|
|
||||||
- Adds download batching and adaptive pipeline depth.
|
|
||||||
- Supports chunks up to 1 MiB.
|
|
||||||
- Token remains optional.
|
|
||||||
- TCP/53 remains the default server transport.
|
|
||||||
- Reconnect is optional: persistent, Auto, or forced rotation.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# DragonTCP Hybrid v3 SafeSpeed
|
|
||||||
|
|
||||||
- Exact Hybrid v1 wire encoding retained.
|
|
||||||
- Download pipeline starts at 256 records.
|
|
||||||
- Server cap remains the v1 value of 256 records.
|
|
||||||
- Upload remains one framed request followed by one response ACK.
|
|
||||||
- No v2 mega-batch behavior.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
bff5e7d9bd9b133ceb90987ad5914f1f26d085ad699cd7d0fc7341e9f713c52f bin/dragontcp-hybrid-server-linux-amd64
|
|
||||||
333718a76ce4a89d968cd7fb4aa74f0365a861a3f0ac36fe933085ca5d67fa11 bin/dragontcp-hybrid-server-linux-arm64
|
|
||||||
94f3fb7cf895b79f0db355ac51b8f11cd14a7d8641e552dfb1b31aad326666ff bin/dragontcp-hybrid-client-linux-amd64
|
|
||||||
af450a117e302eba065feb3a8e4f9b3e0709b3456e821812cb2dacc150041894 android/lib/arm64-v8a/libdragontcp_client.so
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Third-party notices
|
|
||||||
|
|
||||||
DragonTCP Lite VPN includes portions of the Android userspace TCP/IP stack from
|
|
||||||
**FreeProxy** by xVanTuring. The upstream project is licensed under the Apache
|
|
||||||
License, Version 2.0.
|
|
||||||
|
|
||||||
Included/adapted upstream areas:
|
|
||||||
|
|
||||||
- `android/src/tech/xvanturing/freeproxy/vpn/TunnelEngine.kt`
|
|
||||||
- `android/src/tech/xvanturing/freeproxy/vpn/TcpSession.kt`
|
|
||||||
- `android/src/tech/xvanturing/freeproxy/vpn/UdpSession.kt`
|
|
||||||
- `android/src/tech/xvanturing/freeproxy/vpn/TunWriter.kt`
|
|
||||||
- `android/src/tech/xvanturing/freeproxy/vpn/net/*`
|
|
||||||
- the `SocketProtector` interface
|
|
||||||
|
|
||||||
DragonTCP-specific modifications are marked in modified source files. The full
|
|
||||||
Apache 2.0 license is included at `licenses/FreeProxy-APACHE-2.0.txt`.
|
|
||||||
|
|
||||||
The rest of the DragonTCP-specific glue, UI, Go transport, and server code in
|
|
||||||
this bundle is provided as part of this generated project.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
"$ROOT/build_core.sh"
|
|
||||||
"$ROOT/android/build_apk.sh"
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@echo off
|
||||||
|
REM Double-clickable wrapper around build_apk.ps1.
|
||||||
|
REM Any arguments are forwarded, e.g. build_apk.cmd -BuildCore
|
||||||
|
setlocal
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0build_apk.ps1" %*
|
||||||
|
set RC=%ERRORLEVEL%
|
||||||
|
if not "%RC%"=="0" echo.& echo BUILD FAILED (exit %RC%)
|
||||||
|
if "%~1"=="" pause
|
||||||
|
exit /b %RC%
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Windows build for the DragonTCP Lite APK (no Gradle, no Android Studio).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Port of build_apk.sh. Compiles the Kotlin TUN adapter and the Java UI/service,
|
||||||
|
dexes them with d8, packages resources with aapt, aligns with zipalign and
|
||||||
|
signs with apksigner.
|
||||||
|
|
||||||
|
Run it from the directory that holds AndroidManifest.xml. The Go core lives
|
||||||
|
in <repo>/core; the repo root is located by walking up from this script until
|
||||||
|
a core\go.mod is found, so the script keeps working if the tree is nested.
|
||||||
|
|
||||||
|
Everything else is auto-detected where possible:
|
||||||
|
* Android SDK - ANDROID_SDK_ROOT / ANDROID_HOME, or -SdkRoot
|
||||||
|
* build-tools - newest installed version that has aapt/d8/apksigner/zipalign
|
||||||
|
* platform - android-35 if present, else newest installed
|
||||||
|
* JDK - JAVA_HOME, then javac on PATH, then C:\Program Files\Java\*
|
||||||
|
* Kotlin - KOTLIN_HOME, or downloaded once into .\.tools\kotlinc-<version>
|
||||||
|
* native lib - built via <repo>\build_core.ps1 if the .so is missing
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\build_apk.ps1
|
||||||
|
.\build_apk.ps1 -BuildCore # rebuild the Go .so first
|
||||||
|
.\build_apk.ps1 -BuildTools 35.0.0 -Platform android-35
|
||||||
|
.\build_apk.ps1 -Keystore C:\keys\rel.jks -KsPass secret -KeyAlias rel -KeyPass secret
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$SdkRoot,
|
||||||
|
[string]$BuildTools,
|
||||||
|
[string]$Platform,
|
||||||
|
[string]$JavaHome,
|
||||||
|
[string]$KotlinHome,
|
||||||
|
[string]$KotlinVersion = '2.1.21',
|
||||||
|
[string]$RepoRoot,
|
||||||
|
[string]$Keystore,
|
||||||
|
[string]$KsPass = 'dragontcp',
|
||||||
|
[string]$KeyAlias = 'dragontcp',
|
||||||
|
[string]$KeyPass = 'dragontcp',
|
||||||
|
# Rebuild the Go native library before packaging.
|
||||||
|
[switch]$BuildCore,
|
||||||
|
# Fail instead of downloading the Kotlin compiler.
|
||||||
|
[switch]$NoDownload
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$Root = $PSScriptRoot
|
||||||
|
$ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is ~10x faster without it
|
||||||
|
|
||||||
|
function Step { param([string]$m) Write-Host "[apk] $m" -ForegroundColor Cyan }
|
||||||
|
function Die { param([string]$m) throw $m }
|
||||||
|
|
||||||
|
function Invoke-Tool {
|
||||||
|
param([string]$Exe, [string[]]$ToolArgs, [string]$What)
|
||||||
|
# These tools write harmless warnings to stderr (the JVM's sun.misc.Unsafe
|
||||||
|
# notice, apksigner's native-access notice). Under $ErrorActionPreference =
|
||||||
|
# 'Stop' with a merged stream those become terminating errors, so judge
|
||||||
|
# success by the exit code alone.
|
||||||
|
$saved = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
try { & $Exe @ToolArgs } finally { $ErrorActionPreference = $saved }
|
||||||
|
if ($LASTEXITCODE -ne 0) { Die "$What failed (exit $LASTEXITCODE): $Exe" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $Root 'AndroidManifest.xml'))) {
|
||||||
|
Die "No AndroidManifest.xml next to this script. Run it from the Android app directory."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ repo root
|
||||||
|
if (-not $RepoRoot) {
|
||||||
|
# Walk up collecting every ancestor that looks like a checkout. A tree can
|
||||||
|
# contain more than one copy of core\, so prefer the one that also carries
|
||||||
|
# build_core.ps1 and only fall back to the nearest bare core\go.mod.
|
||||||
|
$fallback = $null
|
||||||
|
$probe = $Root
|
||||||
|
for ($i = 0; $i -lt 8 -and $probe; $i++) {
|
||||||
|
if (Test-Path (Join-Path $probe 'core\go.mod')) {
|
||||||
|
if (Test-Path (Join-Path $probe 'build_core.ps1')) { $RepoRoot = $probe; break }
|
||||||
|
if (-not $fallback) { $fallback = $probe }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $probe -Parent
|
||||||
|
if ($parent -eq $probe) { break }
|
||||||
|
$probe = $parent
|
||||||
|
}
|
||||||
|
if (-not $RepoRoot) { $RepoRoot = $fallback }
|
||||||
|
}
|
||||||
|
if ($RepoRoot) { $RepoRoot = (Resolve-Path $RepoRoot).Path }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Android SDK
|
||||||
|
if (-not $SdkRoot) {
|
||||||
|
foreach ($c in $env:ANDROID_SDK_ROOT, $env:ANDROID_HOME,
|
||||||
|
"$env:LOCALAPPDATA\Android\Sdk", 'C:\Android\Sdk') {
|
||||||
|
if ($c -and (Test-Path $c)) { $SdkRoot = $c; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $SdkRoot -or -not (Test-Path $SdkRoot)) {
|
||||||
|
Die "Android SDK not found. Set ANDROID_SDK_ROOT (or pass -SdkRoot 'D:\Android\Sdk')."
|
||||||
|
}
|
||||||
|
$SdkRoot = (Resolve-Path $SdkRoot).Path
|
||||||
|
|
||||||
|
$needTools = @{ aapt = 'aapt.exe'; d8 = 'd8.bat'; apksigner = 'apksigner.bat'; zipalign = 'zipalign.exe' }
|
||||||
|
|
||||||
|
function Test-BuildTools {
|
||||||
|
param([string]$Dir)
|
||||||
|
foreach ($f in $needTools.Values) { if (-not (Test-Path (Join-Path $Dir $f))) { return $false } }
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$btRoot = Join-Path $SdkRoot 'build-tools'
|
||||||
|
if (-not (Test-Path $btRoot)) { Die "No build-tools under $SdkRoot. Install them via the SDK Manager." }
|
||||||
|
if ($BuildTools) {
|
||||||
|
$BT = Join-Path $btRoot $BuildTools
|
||||||
|
if (-not (Test-BuildTools $BT)) { Die "build-tools $BuildTools is missing one of: $($needTools.Values -join ', ')" }
|
||||||
|
} else {
|
||||||
|
$candidates = Get-ChildItem $btRoot -Directory | Sort-Object {
|
||||||
|
try { [version]$_.Name } catch { [version]'0.0.0' }
|
||||||
|
} -Descending
|
||||||
|
$BT = $null
|
||||||
|
foreach ($c in $candidates) { if (Test-BuildTools $c.FullName) { $BT = $c.FullName; $BuildTools = $c.Name; break } }
|
||||||
|
if (-not $BT) { Die "No usable build-tools found under $btRoot (need $($needTools.Values -join ', '))." }
|
||||||
|
}
|
||||||
|
|
||||||
|
$platRoot = Join-Path $SdkRoot 'platforms'
|
||||||
|
if ($Platform) {
|
||||||
|
$AJ = Join-Path $platRoot "$Platform\android.jar"
|
||||||
|
} else {
|
||||||
|
$preferred = Join-Path $platRoot 'android-35\android.jar'
|
||||||
|
if (Test-Path $preferred) {
|
||||||
|
$Platform = 'android-35'; $AJ = $preferred
|
||||||
|
} else {
|
||||||
|
$p = Get-ChildItem $platRoot -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { Test-Path (Join-Path $_.FullName 'android.jar') } |
|
||||||
|
Sort-Object { [int]($_.Name -replace '\D', '') } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $p) { Die "No android.jar found under $platRoot. Install a platform via the SDK Manager." }
|
||||||
|
$Platform = $p.Name; $AJ = Join-Path $p.FullName 'android.jar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $AJ)) { Die "Android platform jar not found: $AJ" }
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------- JDK
|
||||||
|
if (-not $JavaHome) {
|
||||||
|
if ($env:JAVA_HOME -and (Test-Path "$env:JAVA_HOME\bin\javac.exe")) {
|
||||||
|
$JavaHome = $env:JAVA_HOME
|
||||||
|
} else {
|
||||||
|
$jc = Get-Command javac.exe -ErrorAction SilentlyContinue
|
||||||
|
# javapath\javac.exe is a shim: it can compile but has no jar/keytool next to it.
|
||||||
|
if ($jc -and (Test-Path (Join-Path (Split-Path $jc.Source) 'jar.exe'))) {
|
||||||
|
$JavaHome = Split-Path (Split-Path $jc.Source)
|
||||||
|
} else {
|
||||||
|
$j = Get-ChildItem 'C:\Program Files\Java', 'C:\Program Files\Eclipse Adoptium',
|
||||||
|
'C:\Program Files\Microsoft', 'C:\Program Files\Android\Android Studio' `
|
||||||
|
-Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { Test-Path (Join-Path $_.FullName 'bin\javac.exe') } |
|
||||||
|
Sort-Object Name -Descending | Select-Object -First 1
|
||||||
|
if ($j) { $JavaHome = $j.FullName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $JavaHome -or -not (Test-Path "$JavaHome\bin\javac.exe")) {
|
||||||
|
Die "No JDK found. Install a JDK (17 or 21 recommended) and set JAVA_HOME, or pass -JavaHome."
|
||||||
|
}
|
||||||
|
$JavaHome = (Resolve-Path $JavaHome).Path
|
||||||
|
$javaExe = "$JavaHome\bin\java.exe"
|
||||||
|
$javac = "$JavaHome\bin\javac.exe"
|
||||||
|
$jar = "$JavaHome\bin\jar.exe"
|
||||||
|
$keytool = "$JavaHome\bin\keytool.exe"
|
||||||
|
foreach ($t in $javaExe, $javac, $jar, $keytool) { if (-not (Test-Path $t)) { Die "Missing $t (a JRE is not enough - a full JDK is required)." } }
|
||||||
|
# apksigner.bat and d8.bat resolve java through JAVA_HOME.
|
||||||
|
$env:JAVA_HOME = $JavaHome
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- Kotlin
|
||||||
|
function Resolve-KotlinHome {
|
||||||
|
param([string]$Dir)
|
||||||
|
if (-not $Dir) { return $null }
|
||||||
|
if (Test-Path (Join-Path $Dir 'bin\kotlinc.bat')) { return (Resolve-Path $Dir).Path }
|
||||||
|
# Accept the parent of an extracted kotlin-compiler zip too.
|
||||||
|
$inner = Join-Path $Dir 'kotlinc'
|
||||||
|
if (Test-Path (Join-Path $inner 'bin\kotlinc.bat')) { return (Resolve-Path $inner).Path }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$toolsDir = Join-Path $Root '.tools'
|
||||||
|
if (-not $KotlinHome) {
|
||||||
|
foreach ($c in $env:KOTLIN_HOME, (Join-Path $toolsDir "kotlinc-$KotlinVersion")) {
|
||||||
|
$r = Resolve-KotlinHome $c
|
||||||
|
if ($r) { $KotlinHome = $r; break }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$KotlinHome = Resolve-KotlinHome $KotlinHome
|
||||||
|
if (-not $KotlinHome) { Die "No bin\kotlinc.bat under the -KotlinHome you passed." }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $KotlinHome) {
|
||||||
|
if ($NoDownload) { Die "Kotlin compiler not found. Set KOTLIN_HOME or drop -NoDownload." }
|
||||||
|
$dest = Join-Path $toolsDir "kotlinc-$KotlinVersion"
|
||||||
|
$zip = Join-Path $toolsDir "kotlin-compiler-$KotlinVersion.zip"
|
||||||
|
$url = "https://github.com/JetBrains/kotlin/releases/download/v$KotlinVersion/kotlin-compiler-$KotlinVersion.zip"
|
||||||
|
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
|
||||||
|
Step "Downloading Kotlin $KotlinVersion (~85 MB, one time)..."
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||||
|
} catch {
|
||||||
|
Die "Kotlin download failed: $($_.Exception.Message)`nDownload $url manually, extract it, and pass -KotlinHome <dir>\kotlinc."
|
||||||
|
}
|
||||||
|
Step 'Extracting Kotlin...'
|
||||||
|
if (Test-Path $dest) { Remove-Item $dest -Recurse -Force }
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $dest)
|
||||||
|
Remove-Item $zip -Force
|
||||||
|
$KotlinHome = Resolve-KotlinHome $dest
|
||||||
|
if (-not $KotlinHome) { Die "Extracted Kotlin to $dest but found no bin\kotlinc.bat." }
|
||||||
|
}
|
||||||
|
|
||||||
|
# kotlinc.bat is deliberately NOT used: cmd.exe treats ';' as an argument
|
||||||
|
# delimiter, which shreds any -classpath we hand it. Drive the compiler jar directly.
|
||||||
|
$kotlinCompilerJar = Join-Path $KotlinHome 'lib\kotlin-compiler.jar'
|
||||||
|
if (-not (Test-Path $kotlinCompilerJar)) { Die "Missing $kotlinCompilerJar." }
|
||||||
|
$coro = Join-Path $KotlinHome 'lib\kotlinx-coroutines-core-jvm.jar'
|
||||||
|
$stdlib = @('kotlin-stdlib.jar', 'kotlin-stdlib-jdk7.jar', 'kotlin-stdlib-jdk8.jar') |
|
||||||
|
ForEach-Object { Join-Path $KotlinHome "lib\$_" } | Where-Object { Test-Path $_ }
|
||||||
|
if (-not (Test-Path $coro)) { Die "Missing $coro - use a Kotlin distribution that bundles kotlinx-coroutines-core-jvm.jar." }
|
||||||
|
if (-not $stdlib) { Die "No kotlin-stdlib jars under $KotlinHome\lib." }
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- native lib
|
||||||
|
$libDir = Join-Path $Root 'lib\arm64-v8a'
|
||||||
|
$so = Join-Path $libDir 'libdragontcp_client.so'
|
||||||
|
if ($BuildCore -or -not (Test-Path $so)) {
|
||||||
|
$why = if ($BuildCore) { '-BuildCore was requested' } else { 'libdragontcp_client.so is missing' }
|
||||||
|
if (-not $RepoRoot) { Die "$why but no core\go.mod was found above $Root. Pass -RepoRoot." }
|
||||||
|
$coreScript = Join-Path $RepoRoot 'build_core.ps1'
|
||||||
|
if (-not (Test-Path $coreScript)) { Die "$why but $coreScript was not found. Pass -RepoRoot." }
|
||||||
|
Step 'Native core (Go)...'
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $coreScript -ClientOnly -AndroidLibDir $libDir
|
||||||
|
if ($LASTEXITCODE -ne 0) { Die 'build_core.ps1 failed.' }
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $so)) { Die "Missing $so - build the Go core first." }
|
||||||
|
|
||||||
|
Write-Host "app dir : $Root"
|
||||||
|
Write-Host "repo root : $(if ($RepoRoot) { $RepoRoot } else { '<not found>' })"
|
||||||
|
Write-Host "SDK : $SdkRoot"
|
||||||
|
Write-Host "build-tools: $BuildTools"
|
||||||
|
Write-Host "platform : $Platform"
|
||||||
|
Write-Host "JDK : $JavaHome"
|
||||||
|
Write-Host "Kotlin : $KotlinHome"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- build
|
||||||
|
$B = Join-Path $Root 'build'
|
||||||
|
if (Test-Path $B) { Remove-Item $B -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Force -Path "$B\kclasses", "$B\jclasses", "$B\dex" | Out-Null
|
||||||
|
|
||||||
|
Step 'Resources...'
|
||||||
|
Invoke-Tool (Join-Path $BT 'aapt.exe') @(
|
||||||
|
'package', '-f',
|
||||||
|
'-M', (Join-Path $Root 'AndroidManifest.xml'),
|
||||||
|
'-S', (Join-Path $Root 'res'),
|
||||||
|
'-A', (Join-Path $Root 'assets'),
|
||||||
|
'-I', $AJ,
|
||||||
|
'-F', "$B\resources.ap_"
|
||||||
|
) 'aapt'
|
||||||
|
|
||||||
|
Step 'Kotlin TUN adapter...'
|
||||||
|
$ktSources = Get-ChildItem "$Root\src" -Recurse -Filter *.kt | ForEach-Object { $_.FullName }
|
||||||
|
if (-not $ktSources) { Die "No .kt sources under $Root\src." }
|
||||||
|
$CP = (@($AJ, $coro) + $stdlib) -join ';'
|
||||||
|
Invoke-Tool $javaExe (@(
|
||||||
|
'-Xmx2g', '-Dfile.encoding=UTF-8', '-Djava.awt.headless=true',
|
||||||
|
'-cp', $kotlinCompilerJar, 'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler',
|
||||||
|
'-nowarn', '-no-stdlib', '-jvm-target', '1.8',
|
||||||
|
'-kotlin-home', $KotlinHome,
|
||||||
|
'-classpath', $CP, '-d', "$B\kclasses"
|
||||||
|
) + $ktSources) 'kotlinc'
|
||||||
|
|
||||||
|
Step 'Java UI/service...'
|
||||||
|
$javaSources = Get-ChildItem "$Root\src" -Recurse -Filter *.java | ForEach-Object { $_.FullName }
|
||||||
|
if (-not $javaSources) { Die "No .java sources under $Root\src." }
|
||||||
|
$JCP = (@($AJ, $coro, "$B\kclasses") + $stdlib) -join ';'
|
||||||
|
# --release 8 keeps the API surface at Java 8; -bootclasspath is rejected by modern javac.
|
||||||
|
Invoke-Tool $javac (@('-nowarn', '--release', '8', '-classpath', $JCP, '-d', "$B\jclasses") + $javaSources) 'javac'
|
||||||
|
|
||||||
|
Invoke-Tool $jar @('cf', "$B\kclasses.jar", '-C', "$B\kclasses", '.') 'jar (kotlin)'
|
||||||
|
Invoke-Tool $jar @('cf', "$B\jclasses.jar", '-C', "$B\jclasses", '.') 'jar (java)'
|
||||||
|
|
||||||
|
Step 'DEX...'
|
||||||
|
Invoke-Tool (Join-Path $BT 'd8.bat') (@(
|
||||||
|
'--lib', $AJ, '--min-api', '29', '--output', "$B\dex",
|
||||||
|
"$B\kclasses.jar", "$B\jclasses.jar", $coro
|
||||||
|
) + $stdlib) 'd8'
|
||||||
|
|
||||||
|
Step 'Packaging...'
|
||||||
|
$unsigned = "$B\DragonTCP-Hybrid-unsigned.apk"
|
||||||
|
Copy-Item "$B\resources.ap_" $unsigned -Force
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
$zipFile = [System.IO.Compression.ZipFile]::Open($unsigned, 'Update')
|
||||||
|
try {
|
||||||
|
$level = [System.IO.Compression.CompressionLevel]::Optimal
|
||||||
|
foreach ($dex in Get-ChildItem "$B\dex" -Filter 'classes*.dex') {
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipFile, $dex.FullName, $dex.Name, $level) | Out-Null
|
||||||
|
}
|
||||||
|
$libRoot = Join-Path $Root 'lib'
|
||||||
|
foreach ($f in Get-ChildItem $libRoot -Recurse -File) {
|
||||||
|
$entry = 'lib/' + $f.FullName.Substring($libRoot.Length + 1).Replace('\', '/')
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipFile, $f.FullName, $entry, $level) | Out-Null
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$zipFile.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
Step 'Aligning...'
|
||||||
|
$aligned = "$B\DragonTCP-Hybrid-aligned.apk"
|
||||||
|
Invoke-Tool (Join-Path $BT 'zipalign.exe') @('-p', '-f', '4', $unsigned, $aligned) 'zipalign'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- signing
|
||||||
|
if (-not $Keystore) { $Keystore = Join-Path $Root 'dragontcp-lite-debug.jks' }
|
||||||
|
if (-not (Test-Path $Keystore)) {
|
||||||
|
Step 'Generating debug keystore...'
|
||||||
|
Invoke-Tool $keytool @(
|
||||||
|
'-genkeypair', '-keystore', $Keystore, '-storepass', $KsPass, '-keypass', $KeyPass,
|
||||||
|
'-alias', $KeyAlias, '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000',
|
||||||
|
'-dname', 'CN=DragonTCP Lite,O=DragonTCP,C=US'
|
||||||
|
) 'keytool'
|
||||||
|
}
|
||||||
|
|
||||||
|
Step 'Signing...'
|
||||||
|
$out = "$B\DragonTCP-Hybrid-arm64.apk"
|
||||||
|
Invoke-Tool (Join-Path $BT 'apksigner.bat') @(
|
||||||
|
'sign', '--ks', $Keystore, '--ks-pass', "pass:$KsPass", '--key-pass', "pass:$KeyPass",
|
||||||
|
'--out', $out, $aligned
|
||||||
|
) 'apksigner sign'
|
||||||
|
Invoke-Tool (Join-Path $BT 'apksigner.bat') @('verify', '--verbose', $out) 'apksigner verify'
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "APK: $out" -ForegroundColor Green
|
||||||
|
Write-Host ("Size: {0:N1} MB" -f ((Get-Item $out).Length / 1MB))
|
||||||
|
Write-Host "Install with: adb install -r `"$out`""
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
GO_BIN="${GO_BIN:-go}"
|
|
||||||
command -v "$GO_BIN" >/dev/null 2>&1 || { echo "Go compiler not found" >&2; exit 1; }
|
|
||||||
mkdir -p "$ROOT/bin" "$ROOT/android/lib/arm64-v8a"
|
|
||||||
cd "$ROOT/core"
|
|
||||||
|
|
||||||
echo "[core] Android ARM64 client..."
|
|
||||||
CGO_ENABLED=0 GOOS=android GOARCH=arm64 "$GO_BIN" build -trimpath -ldflags='-s -w' \
|
|
||||||
-o "$ROOT/android/lib/arm64-v8a/libdragontcp_client.so" ./cmd/dragontcp-client
|
|
||||||
|
|
||||||
echo "[core] Linux AMD64 server..."
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 "$GO_BIN" build -trimpath -ldflags='-s -w' \
|
|
||||||
-o "$ROOT/bin/dragontcp-hybrid-server-linux-amd64" ./cmd/dragontcp-server
|
|
||||||
|
|
||||||
echo "[core] Linux ARM64 server..."
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 "$GO_BIN" build -trimpath -ldflags='-s -w' \
|
|
||||||
-o "$ROOT/bin/dragontcp-hybrid-server-linux-arm64" ./cmd/dragontcp-server
|
|
||||||
|
|
||||||
echo "Core build complete."
|
|
||||||
@@ -1,791 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"dragontcp/internal/protocol"
|
|
||||||
"dragontcp/internal/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
type chunkClientOptions struct {
|
|
||||||
startSize int
|
|
||||||
minSize int
|
|
||||||
maxSize int
|
|
||||||
adaptive bool
|
|
||||||
adaptSuccesses int
|
|
||||||
adaptLog bool
|
|
||||||
pollers int
|
|
||||||
reconnectEvery int
|
|
||||||
pollDelay time.Duration
|
|
||||||
txnTimeout time.Duration
|
|
||||||
tcpBuffer int
|
|
||||||
}
|
|
||||||
|
|
||||||
type adaptiveSizer struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
name string
|
|
||||||
current int
|
|
||||||
min int
|
|
||||||
max int
|
|
||||||
adaptive bool
|
|
||||||
adaptSuccesses int
|
|
||||||
successes int
|
|
||||||
good int
|
|
||||||
bad int
|
|
||||||
logChanges bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAdaptiveSizer(name string, start int, opts chunkClientOptions) *adaptiveSizer {
|
|
||||||
if start < opts.minSize {
|
|
||||||
start = opts.minSize
|
|
||||||
}
|
|
||||||
if start > opts.maxSize {
|
|
||||||
start = opts.maxSize
|
|
||||||
}
|
|
||||||
return &adaptiveSizer{
|
|
||||||
name: name,
|
|
||||||
current: start,
|
|
||||||
min: opts.minSize,
|
|
||||||
max: opts.maxSize,
|
|
||||||
adaptive: opts.adaptive,
|
|
||||||
adaptSuccesses: func() int {
|
|
||||||
if opts.adaptSuccesses > 0 {
|
|
||||||
return opts.adaptSuccesses
|
|
||||||
}
|
|
||||||
return 64
|
|
||||||
}(),
|
|
||||||
logChanges: opts.adaptLog,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *adaptiveSizer) Current() int {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.current
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *adaptiveSizer) Success(attempted int) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if !s.adaptive || attempted != s.current || s.current >= s.max {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if attempted > s.good {
|
|
||||||
s.good = attempted
|
|
||||||
}
|
|
||||||
s.successes++
|
|
||||||
growAfter := s.adaptSuccesses
|
|
||||||
if s.bad > 0 && s.bad-s.good <= 64 {
|
|
||||||
growAfter *= 8
|
|
||||||
}
|
|
||||||
if s.successes < growAfter {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.successes = 0
|
|
||||||
|
|
||||||
old := s.current
|
|
||||||
next := 0
|
|
||||||
if s.bad > old+1 {
|
|
||||||
next = old + (s.bad-old)/2
|
|
||||||
} else {
|
|
||||||
if s.bad > 0 {
|
|
||||||
s.bad = 0
|
|
||||||
}
|
|
||||||
step := old / 4
|
|
||||||
if step < 32 {
|
|
||||||
step = 32
|
|
||||||
}
|
|
||||||
next = old + step
|
|
||||||
}
|
|
||||||
if next > s.max {
|
|
||||||
next = s.max
|
|
||||||
}
|
|
||||||
if next <= old {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.current = next
|
|
||||||
if s.logChanges {
|
|
||||||
fmt.Printf("adaptive %s chunk: %d -> %d after stable success\n", s.name, old, next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *adaptiveSizer) Failure(attempted int) (int, int) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
old := s.current
|
|
||||||
if !s.adaptive || attempted != s.current {
|
|
||||||
return old, old
|
|
||||||
}
|
|
||||||
s.successes = 0
|
|
||||||
if s.bad == 0 || attempted < s.bad {
|
|
||||||
s.bad = attempted
|
|
||||||
}
|
|
||||||
next := attempted / 2
|
|
||||||
if s.good > 0 && s.good < attempted {
|
|
||||||
next = s.good
|
|
||||||
} else {
|
|
||||||
s.good = 0
|
|
||||||
}
|
|
||||||
if next < s.min {
|
|
||||||
next = s.min
|
|
||||||
}
|
|
||||||
if next >= attempted && attempted > s.min {
|
|
||||||
next = attempted - 1
|
|
||||||
}
|
|
||||||
if next < s.min {
|
|
||||||
next = s.min
|
|
||||||
}
|
|
||||||
s.current = next
|
|
||||||
if s.logChanges && old != next {
|
|
||||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
|
||||||
}
|
|
||||||
return old, next
|
|
||||||
}
|
|
||||||
|
|
||||||
type physicalConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
requests int
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestLane struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
serverAddr string
|
|
||||||
tcpBuffer int
|
|
||||||
reconnectEvery int
|
|
||||||
timeout time.Duration
|
|
||||||
pc *physicalConn
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *requestLane {
|
|
||||||
return &requestLane{
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
tcpBuffer: tcpBuffer,
|
|
||||||
reconnectEvery: reconnectEvery,
|
|
||||||
timeout: timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *requestLane) discardLocked() {
|
|
||||||
if l.pc != nil {
|
|
||||||
_ = l.pc.conn.Close()
|
|
||||||
l.pc = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *requestLane) closeAfterLocked() {
|
|
||||||
if l.pc != nil && l.reconnectEvery > 0 && l.pc.requests >= l.reconnectEvery {
|
|
||||||
l.discardLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *requestLane) ensureLocked() error {
|
|
||||||
if l.closed {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
if l.pc != nil {
|
|
||||||
if l.reconnectEvery <= 0 || l.pc.requests < l.reconnectEvery {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
l.discardLocked()
|
|
||||||
}
|
|
||||||
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
|
||||||
conn, err := d.Dial("tcp", l.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
protocol.TuneTCP(conn)
|
|
||||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
|
||||||
l.pc = &physicalConn{conn: conn}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *requestLane) Close() {
|
|
||||||
l.mu.Lock()
|
|
||||||
l.closed = true
|
|
||||||
l.discardLocked()
|
|
||||||
l.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte) (byte, []byte, error) {
|
|
||||||
l.mu.Lock()
|
|
||||||
defer l.mu.Unlock()
|
|
||||||
if err := l.ensureLocked(); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
timeout := l.timeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
if err := wire.WriteRequest(l.pc.conn, mode, sid, seq, payload); err != nil {
|
|
||||||
l.discardLocked()
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
|
||||||
if err != nil {
|
|
||||||
l.discardLocked()
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
l.pc.requests++
|
|
||||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
|
||||||
l.closeAfterLocked()
|
|
||||||
if status != wire.StatusError && len(body) > 0 {
|
|
||||||
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
|
|
||||||
}
|
|
||||||
return status, body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// download sends one compact request and consumes up to count response records.
|
|
||||||
// startOffset is also the response keystream sequence. Each DATA response advances
|
|
||||||
// it by exactly the returned byte count.
|
|
||||||
func (l *requestLane) download(sid wire.SessionID, startOffset, ackOffset uint64, maxChunk, count int) ([][]byte, byte, error) {
|
|
||||||
l.mu.Lock()
|
|
||||||
defer l.mu.Unlock()
|
|
||||||
if err := l.ensureLocked(); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
timeout := l.timeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
|
|
||||||
payload := make([]byte, 14)
|
|
||||||
binary.BigEndian.PutUint64(payload[0:8], ackOffset)
|
|
||||||
binary.BigEndian.PutUint32(payload[8:12], uint32(maxChunk))
|
|
||||||
binary.BigEndian.PutUint16(payload[12:14], uint16(count))
|
|
||||||
if err := wire.WriteRequest(l.pc.conn, wire.ModeDownload, sid, startOffset, payload); err != nil {
|
|
||||||
l.discardLocked()
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([][]byte, 0, count)
|
|
||||||
offset := startOffset
|
|
||||||
lastStatus := wire.StatusOK
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
|
||||||
if err != nil {
|
|
||||||
l.discardLocked()
|
|
||||||
return out, lastStatus, err
|
|
||||||
}
|
|
||||||
lastStatus = status
|
|
||||||
switch status {
|
|
||||||
case wire.StatusData:
|
|
||||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
|
|
||||||
if len(body) == 0 {
|
|
||||||
l.discardLocked()
|
|
||||||
return out, status, fmt.Errorf("empty DATA response")
|
|
||||||
}
|
|
||||||
out = append(out, body)
|
|
||||||
offset += uint64(len(body))
|
|
||||||
case wire.StatusWait, wire.StatusEOF:
|
|
||||||
i = count // stop after this response
|
|
||||||
case wire.StatusError:
|
|
||||||
l.discardLocked()
|
|
||||||
return out, status, fmt.Errorf("%s", string(body))
|
|
||||||
default:
|
|
||||||
l.discardLocked()
|
|
||||||
return out, status, fmt.Errorf("unknown response status %d", status)
|
|
||||||
}
|
|
||||||
if status == wire.StatusWait || status == wire.StatusEOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.pc.requests++
|
|
||||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
|
||||||
l.closeAfterLocked()
|
|
||||||
return out, lastStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pathProfile struct {
|
|
||||||
upload int
|
|
||||||
download int
|
|
||||||
persistent bool
|
|
||||||
at time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
var profileState struct {
|
|
||||||
sync.Mutex
|
|
||||||
key string
|
|
||||||
p pathProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
var probeSeq atomic.Uint64
|
|
||||||
|
|
||||||
func randomSessionID() (wire.SessionID, error) {
|
|
||||||
var sid wire.SessionID
|
|
||||||
_, err := rand.Read(sid[:])
|
|
||||||
return sid, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func probePattern(n int) []byte {
|
|
||||||
out := make([]byte, n)
|
|
||||||
for i := range out {
|
|
||||||
out[i] = byte((i*31 + 17) & 0xff)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeProbePayload(kind byte, value, total int, token string) []byte {
|
|
||||||
base := 11 + len(token)
|
|
||||||
if total < base {
|
|
||||||
total = base
|
|
||||||
}
|
|
||||||
out := make([]byte, total)
|
|
||||||
copy(out[:4], wire.ProbeMagic[:])
|
|
||||||
out[4] = kind
|
|
||||||
binary.BigEndian.PutUint16(out[5:7], uint16(len(token)))
|
|
||||||
binary.BigEndian.PutUint32(out[7:11], uint32(value))
|
|
||||||
copy(out[11:11+len(token)], token)
|
|
||||||
for i := base; i < len(out); i++ {
|
|
||||||
out[i] = byte((i*31 + 17) & 0xff)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int) bool {
|
|
||||||
sid, err := randomSessionID()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
timeout := opts.txnTimeout
|
|
||||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
|
||||||
timeout = 2500 * time.Millisecond
|
|
||||||
}
|
|
||||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout)
|
|
||||||
defer lane.Close()
|
|
||||||
seq := probeSeq.Add(1)
|
|
||||||
|
|
||||||
total := 0
|
|
||||||
value := candidate
|
|
||||||
if kind == wire.ProbeUpload {
|
|
||||||
total = candidate
|
|
||||||
}
|
|
||||||
payload := makeProbePayload(kind, value, total, token)
|
|
||||||
status, body, err := lane.single(wire.ModeProbe, sid, seq, payload)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if kind == wire.ProbeUpload {
|
|
||||||
return status == wire.StatusOK
|
|
||||||
}
|
|
||||||
if kind == wire.ProbeDownload {
|
|
||||||
if status != wire.StatusData || len(body) != candidate {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
want := probePattern(candidate)
|
|
||||||
for i := range body {
|
|
||||||
if body[i] != want[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return status == wire.StatusOK
|
|
||||||
}
|
|
||||||
|
|
||||||
func probePersistent(serverAddr, token string, opts chunkClientOptions) bool {
|
|
||||||
sid, err := randomSessionID()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
timeout := opts.txnTimeout
|
|
||||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
|
||||||
timeout = 2500 * time.Millisecond
|
|
||||||
}
|
|
||||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout)
|
|
||||||
defer lane.Close()
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
seq := probeSeq.Add(1)
|
|
||||||
payload := makeProbePayload(wire.ProbeKeepalive, i, 32+len(token), token)
|
|
||||||
status, _, err := lane.single(wire.ModeProbe, sid, seq, payload)
|
|
||||||
if err != nil || status != wire.StatusOK {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeCandidates(minSize, maxSize int) []int {
|
|
||||||
base := []int{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}
|
|
||||||
seen := map[int]bool{}
|
|
||||||
out := make([]int, 0, len(base)+2)
|
|
||||||
for _, n := range base {
|
|
||||||
if n >= minSize && n <= maxSize && !seen[n] {
|
|
||||||
out = append(out, n)
|
|
||||||
seen[n] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !seen[minSize] {
|
|
||||||
out = append(out, minSize)
|
|
||||||
}
|
|
||||||
if !seen[maxSize] {
|
|
||||||
out = append(out, maxSize)
|
|
||||||
}
|
|
||||||
sort.Ints(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte) int {
|
|
||||||
candidates := probeCandidates(opts.minSize, opts.maxSize)
|
|
||||||
lo, hi := 0, len(candidates)-1
|
|
||||||
best := opts.minSize
|
|
||||||
for lo <= hi {
|
|
||||||
mid := lo + (hi-lo)/2
|
|
||||||
candidate := candidates[mid]
|
|
||||||
if probeOne(serverAddr, token, opts, kind, candidate) {
|
|
||||||
best = candidate
|
|
||||||
lo = mid + 1
|
|
||||||
} else {
|
|
||||||
hi = mid - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if best < opts.minSize {
|
|
||||||
best = opts.minSize
|
|
||||||
}
|
|
||||||
return best
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile {
|
|
||||||
key := fmt.Sprintf("%s|%s|%d|%d", serverAddr, token, opts.minSize, opts.maxSize)
|
|
||||||
profileState.Lock()
|
|
||||||
if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute {
|
|
||||||
p := profileState.p
|
|
||||||
profileState.Unlock()
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
profileState.Unlock()
|
|
||||||
|
|
||||||
fallbackUp := minInt(opts.maxSize, maxInt(opts.minSize, 32768))
|
|
||||||
fallbackDown := minInt(opts.maxSize, maxInt(opts.minSize, 1350))
|
|
||||||
|
|
||||||
upCh := make(chan int, 1)
|
|
||||||
downCh := make(chan int, 1)
|
|
||||||
go func() { upCh <- probeMaximum(serverAddr, token, opts, wire.ProbeUpload) }()
|
|
||||||
go func() { downCh <- probeMaximum(serverAddr, token, opts, wire.ProbeDownload) }()
|
|
||||||
|
|
||||||
p := pathProfile{upload: fallbackUp, download: fallbackDown, persistent: false, at: time.Now()}
|
|
||||||
select {
|
|
||||||
case p.upload = <-upCh:
|
|
||||||
case <-time.After(20 * time.Second):
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case p.download = <-downCh:
|
|
||||||
case <-time.After(20 * time.Second):
|
|
||||||
}
|
|
||||||
p.persistent = probePersistent(serverAddr, token, opts)
|
|
||||||
|
|
||||||
fmt.Printf("path probe: upload=%d download=%d persistent=%t\n", p.upload, p.download, p.persistent)
|
|
||||||
|
|
||||||
profileState.Lock()
|
|
||||||
profileState.key = key
|
|
||||||
profileState.p = p
|
|
||||||
profileState.Unlock()
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeOpen(token, host string, port int) ([]byte, error) {
|
|
||||||
if len(token) > 65535 || len(host) > 65535 {
|
|
||||||
return nil, fmt.Errorf("token or hostname too long")
|
|
||||||
}
|
|
||||||
out := make([]byte, 6+len(token)+len(host))
|
|
||||||
binary.BigEndian.PutUint16(out[0:2], uint16(len(token)))
|
|
||||||
binary.BigEndian.PutUint16(out[2:4], uint16(len(host)))
|
|
||||||
binary.BigEndian.PutUint16(out[4:6], uint16(port))
|
|
||||||
copy(out[6:6+len(token)], token)
|
|
||||||
copy(out[6+len(token):], host)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type chunkConn struct {
|
|
||||||
sid wire.SessionID
|
|
||||||
opts chunkClientOptions
|
|
||||||
uploadLane *requestLane
|
|
||||||
downloadLane *requestLane
|
|
||||||
serverMax int
|
|
||||||
upSizer *adaptiveSizer
|
|
||||||
downSizer *adaptiveSizer
|
|
||||||
|
|
||||||
writeMu sync.Mutex
|
|
||||||
upOffset uint64
|
|
||||||
|
|
||||||
readMu sync.Mutex
|
|
||||||
readBuf []byte
|
|
||||||
downloadOffset uint64
|
|
||||||
consumedOffset uint64
|
|
||||||
eof bool
|
|
||||||
pipeline int
|
|
||||||
maxPipeline int
|
|
||||||
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
|
|
||||||
if opts.minSize < 32 {
|
|
||||||
opts.minSize = 32
|
|
||||||
}
|
|
||||||
if opts.maxSize < opts.minSize {
|
|
||||||
opts.maxSize = opts.minSize
|
|
||||||
}
|
|
||||||
if opts.maxSize > 1024*1024 {
|
|
||||||
opts.maxSize = 1024 * 1024
|
|
||||||
}
|
|
||||||
if opts.txnTimeout <= 0 {
|
|
||||||
opts.txnTimeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
if opts.adaptSuccesses < 1 {
|
|
||||||
opts.adaptSuccesses = 64
|
|
||||||
}
|
|
||||||
if opts.reconnectEvery < 0 {
|
|
||||||
opts.reconnectEvery = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
profile := getPathProfile(serverAddr, token, opts)
|
|
||||||
reconnect := opts.reconnectEvery
|
|
||||||
// Compatibility-friendly reconnect modes:
|
|
||||||
// 0 = persistent (CLI explicit)
|
|
||||||
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
|
|
||||||
// N>=2 = force connection rotation after N logical requests
|
|
||||||
if reconnect == 1 {
|
|
||||||
if profile.persistent {
|
|
||||||
reconnect = 0
|
|
||||||
fmt.Printf("path probe: reconnect mode auto -> persistent\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("path probe: reconnect mode auto -> every request\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sid, err := randomSessionID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
control := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
|
||||||
payload, err := encodeOpen(token, targetHost, targetPort)
|
|
||||||
if err != nil {
|
|
||||||
control.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
status, body, err := control.single(wire.ModeOpen, sid, 0, payload)
|
|
||||||
if err != nil {
|
|
||||||
control.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if status == wire.StatusError {
|
|
||||||
control.Close()
|
|
||||||
return nil, fmt.Errorf("%s", string(body))
|
|
||||||
}
|
|
||||||
if status != wire.StatusOK || len(body) != 4 {
|
|
||||||
control.Close()
|
|
||||||
return nil, fmt.Errorf("bad OPEN response")
|
|
||||||
}
|
|
||||||
serverMax := int(binary.BigEndian.Uint32(body))
|
|
||||||
control.Close()
|
|
||||||
if serverMax < opts.minSize {
|
|
||||||
return nil, fmt.Errorf("server maximum chunk %d is below client minimum %d", serverMax, opts.minSize)
|
|
||||||
}
|
|
||||||
if opts.maxSize > serverMax {
|
|
||||||
opts.maxSize = serverMax
|
|
||||||
}
|
|
||||||
upStart := minInt(profile.upload, opts.maxSize)
|
|
||||||
downStart := minInt(profile.download, opts.maxSize)
|
|
||||||
if upStart < opts.minSize {
|
|
||||||
upStart = opts.minSize
|
|
||||||
}
|
|
||||||
if downStart < opts.minSize {
|
|
||||||
downStart = opts.minSize
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &chunkConn{
|
|
||||||
sid: sid,
|
|
||||||
opts: opts,
|
|
||||||
serverMax: serverMax,
|
|
||||||
uploadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
|
||||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
|
||||||
// BHTTP-style safe pipeline: request up to 256 records immediately.
|
|
||||||
// Hybrid v1 already supported 256 on the wire/server; starting at 32
|
|
||||||
// made tiny-path downloads spend many RTTs ramping up.
|
|
||||||
pipeline: 256,
|
|
||||||
maxPipeline: 256,
|
|
||||||
}
|
|
||||||
c.upSizer = newAdaptiveSizer("upload", upStart, opts)
|
|
||||||
c.downSizer = newAdaptiveSizer("download", downStart, opts)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *chunkConn) fillReadBuffer() error {
|
|
||||||
if c.eof {
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
minFailures := 0
|
|
||||||
for len(c.readBuf) == 0 && !c.eof {
|
|
||||||
chunk := c.downSizer.Current()
|
|
||||||
count := c.pipeline
|
|
||||||
if count < 1 {
|
|
||||||
count = 1
|
|
||||||
}
|
|
||||||
if count > c.maxPipeline {
|
|
||||||
count = c.maxPipeline
|
|
||||||
}
|
|
||||||
// Bound each batch to roughly 1 MiB of useful data.
|
|
||||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
|
||||||
count = maxInt(maxCount, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
|
||||||
for _, part := range data {
|
|
||||||
c.readBuf = append(c.readBuf, part...)
|
|
||||||
c.downloadOffset += uint64(len(part))
|
|
||||||
}
|
|
||||||
if len(data) > 0 {
|
|
||||||
c.downSizer.Success(chunk)
|
|
||||||
if c.pipeline < c.maxPipeline {
|
|
||||||
c.pipeline++
|
|
||||||
}
|
|
||||||
minFailures = 0
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if c.pipeline > 1 {
|
|
||||||
old := c.pipeline
|
|
||||||
c.pipeline /= 2
|
|
||||||
if c.pipeline < 1 {
|
|
||||||
c.pipeline = 1
|
|
||||||
}
|
|
||||||
if c.opts.adaptLog && old != c.pipeline {
|
|
||||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure\n", old, c.pipeline)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
old, next := c.downSizer.Failure(chunk)
|
|
||||||
if old == next && next == c.opts.minSize {
|
|
||||||
minFailures++
|
|
||||||
if minFailures >= 8 {
|
|
||||||
return fmt.Errorf("download failed at minimum chunk %d: %w", next, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
if len(c.readBuf) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case wire.StatusEOF:
|
|
||||||
c.eof = true
|
|
||||||
case wire.StatusWait:
|
|
||||||
if c.opts.pollDelay > 0 {
|
|
||||||
time.Sleep(c.opts.pollDelay)
|
|
||||||
} else {
|
|
||||||
time.Sleep(5 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(c.readBuf) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.eof && len(c.readBuf) == 0 {
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *chunkConn) Read(p []byte) (int, error) {
|
|
||||||
c.readMu.Lock()
|
|
||||||
defer c.readMu.Unlock()
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if len(c.readBuf) == 0 {
|
|
||||||
if err := c.fillReadBuffer(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n := copy(p, c.readBuf)
|
|
||||||
c.readBuf = c.readBuf[n:]
|
|
||||||
c.consumedOffset += uint64(n)
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *chunkConn) Write(p []byte) (int, error) {
|
|
||||||
c.writeMu.Lock()
|
|
||||||
defer c.writeMu.Unlock()
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
total := 0
|
|
||||||
minFailures := 0
|
|
||||||
for len(p) > 0 {
|
|
||||||
size := c.upSizer.Current()
|
|
||||||
n := minInt(size, len(p))
|
|
||||||
status, body, err := c.uploadLane.single(wire.ModeUpload, c.sid, c.upOffset, p[:n])
|
|
||||||
if err != nil {
|
|
||||||
old, next := c.upSizer.Failure(size)
|
|
||||||
if old == next && next == c.opts.minSize {
|
|
||||||
minFailures++
|
|
||||||
if minFailures >= 8 {
|
|
||||||
return total, fmt.Errorf("upload failed at minimum chunk %d: %w", next, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
minFailures = 0
|
|
||||||
}
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if status == wire.StatusError {
|
|
||||||
return total, fmt.Errorf("%s", string(body))
|
|
||||||
}
|
|
||||||
if status != wire.StatusOK {
|
|
||||||
return total, fmt.Errorf("unexpected upload status %d", status)
|
|
||||||
}
|
|
||||||
c.upOffset += uint64(n)
|
|
||||||
total += n
|
|
||||||
p = p[n:]
|
|
||||||
c.upSizer.Success(size)
|
|
||||||
minFailures = 0
|
|
||||||
}
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *chunkConn) Close() error {
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
lane := newRequestLane(c.uploadLane.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
|
||||||
_, _, _ = lane.single(wire.ModeClose, c.sid, 0, nil)
|
|
||||||
lane.Close()
|
|
||||||
c.uploadLane.Close()
|
|
||||||
c.downloadLane.Close()
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-binary-local") }
|
|
||||||
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-binary-remote") }
|
|
||||||
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
|
|
||||||
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
|
|
||||||
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
|
|
||||||
|
|
||||||
type dummyAddr string
|
|
||||||
|
|
||||||
func (d dummyAddr) Network() string { return "dragontcp-binary" }
|
|
||||||
func (d dummyAddr) String() string { return string(d) }
|
|
||||||
|
|
||||||
func minInt(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
func maxInt(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
|
||||||
opts := chunkClientOptions{
|
|
||||||
startSize: 64,
|
|
||||||
minSize: 32,
|
|
||||||
maxSize: 1024,
|
|
||||||
adaptive: true,
|
|
||||||
adaptSuccesses: 2,
|
|
||||||
}
|
|
||||||
s := newAdaptiveSizer("test", 64, opts)
|
|
||||||
_, next := s.Failure(64)
|
|
||||||
if next != 32 {
|
|
||||||
t.Fatalf("failure should reduce 64 -> 32, got %d", next)
|
|
||||||
}
|
|
||||||
for i := 0; i < 16; i++ {
|
|
||||||
s.Success(32)
|
|
||||||
}
|
|
||||||
if got := s.Current(); got <= 32 {
|
|
||||||
t.Fatalf("adaptive controller remained stuck at minimum: %d", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReconnectZeroMeansPersistent(t *testing.T) {
|
|
||||||
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
|
|
||||||
if lane.reconnectEvery != 0 {
|
|
||||||
t.Fatalf("reconnectEvery=%d, want 0", lane.reconnectEvery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"dragontcp/internal/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxHeader = 128 * 1024
|
|
||||||
|
|
||||||
var requestCounter atomic.Uint32
|
|
||||||
|
|
||||||
func readHTTPHeaders(conn net.Conn) ([]byte, []byte, error) {
|
|
||||||
buf := make([]byte, 0, 8192)
|
|
||||||
tmp := make([]byte, 8192)
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := conn.Read(tmp)
|
|
||||||
if n > 0 {
|
|
||||||
buf = append(buf, tmp[:n]...)
|
|
||||||
|
|
||||||
if len(buf) > maxHeader {
|
|
||||||
return nil, nil, fmt.Errorf("HTTP headers too large")
|
|
||||||
}
|
|
||||||
|
|
||||||
if i := bytes.Index(buf, []byte("\r\n\r\n")); i >= 0 {
|
|
||||||
end := i + 4
|
|
||||||
return buf[:end], buf[end:], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHostPort(authority string, defaultPort int) (string, int, error) {
|
|
||||||
authority = strings.TrimSpace(authority)
|
|
||||||
|
|
||||||
if host, portText, err := net.SplitHostPort(authority); err == nil {
|
|
||||||
port, err := strconv.Atoi(portText)
|
|
||||||
return host, port, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host without port.
|
|
||||||
if strings.HasPrefix(authority, "[") && strings.HasSuffix(authority, "]") {
|
|
||||||
return strings.Trim(authority, "[]"), defaultPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Count(authority, ":") == 0 {
|
|
||||||
return authority, defaultPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bare IPv6.
|
|
||||||
if ip := net.ParseIP(authority); ip != nil {
|
|
||||||
return authority, defaultPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", 0, fmt.Errorf("invalid authority: %s", authority)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewritePlainHTTPRequest(header []byte) (string, int, []byte, error) {
|
|
||||||
text := string(header)
|
|
||||||
lines := strings.Split(text, "\r\n")
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return "", 0, nil, fmt.Errorf("empty request")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(lines[0], " ", 3)
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return "", 0, nil, fmt.Errorf("invalid request line")
|
|
||||||
}
|
|
||||||
|
|
||||||
method, target, version := parts[0], parts[1], parts[2]
|
|
||||||
|
|
||||||
var (
|
|
||||||
hostHeader string
|
|
||||||
headers []string
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, line := range lines[1:] {
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, ok := strings.Cut(line, ":")
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
lk := strings.ToLower(strings.TrimSpace(k))
|
|
||||||
|
|
||||||
if lk == "host" {
|
|
||||||
hostHeader = strings.TrimSpace(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if lk == "connection" ||
|
|
||||||
lk == "proxy-connection" ||
|
|
||||||
lk == "proxy-authorization" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = append(headers, k+": "+strings.TrimSpace(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(target)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var host string
|
|
||||||
var port int
|
|
||||||
path := target
|
|
||||||
|
|
||||||
if u.Hostname() != "" {
|
|
||||||
if strings.ToLower(u.Scheme) != "http" {
|
|
||||||
return "", 0, nil, fmt.Errorf("unsupported plain HTTP scheme: %s", u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
host = u.Hostname()
|
|
||||||
port = 80
|
|
||||||
|
|
||||||
if u.Port() != "" {
|
|
||||||
port, err = strconv.Atoi(u.Port())
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path = u.EscapedPath()
|
|
||||||
if path == "" {
|
|
||||||
path = "/"
|
|
||||||
}
|
|
||||||
if u.RawQuery != "" {
|
|
||||||
path += "?" + u.RawQuery
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if hostHeader == "" {
|
|
||||||
return "", 0, nil, fmt.Errorf("missing Host header")
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, err = parseHostPort(hostHeader, 80)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, nil, err
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
path = "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var out strings.Builder
|
|
||||||
fmt.Fprintf(&out, "%s %s %s\r\n", method, path, version)
|
|
||||||
|
|
||||||
sawHost := false
|
|
||||||
for _, h := range headers {
|
|
||||||
if strings.HasPrefix(strings.ToLower(h), "host:") {
|
|
||||||
sawHost = true
|
|
||||||
}
|
|
||||||
out.WriteString(h)
|
|
||||||
out.WriteString("\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sawHost {
|
|
||||||
if port == 80 {
|
|
||||||
fmt.Fprintf(&out, "Host: %s\r\n", host)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&out, "Host: %s\r\n", net.JoinHostPort(host, strconv.Itoa(port)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.WriteString("Connection: close\r\n\r\n")
|
|
||||||
|
|
||||||
return host, port, []byte(out.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openDragonTCPTunnel(serverAddr, token, targetHost string, targetPort int, transport string, tcpBuffer int) (net.Conn, error) {
|
|
||||||
d := net.Dialer{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := d.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol.TuneTCP(conn)
|
|
||||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
|
|
||||||
|
|
||||||
// Correlation only; cryptographic randomness is unnecessary here.
|
|
||||||
requestID := requestCounter.Add(1)
|
|
||||||
|
|
||||||
var command []byte
|
|
||||||
if transport == "raw" {
|
|
||||||
command = []byte(fmt.Sprintf("TUNNEL2 %s %s %d RAW", token, targetHost, targetPort))
|
|
||||||
} else {
|
|
||||||
// Legacy XOR command remains compatible with the older server.
|
|
||||||
command = []byte(fmt.Sprintf("TUNNEL %s %s %d", token, targetHost, targetPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := protocol.WriteRequestFrame(conn, requestID, command); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
responseID, response, err := protocol.ReadResponseFrame(conn)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseID != requestID {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("request ID mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(response) != "CONNECTED" {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("%s", response)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Time{})
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHTTPError(conn net.Conn, code int, reason, detail string) {
|
|
||||||
if detail == "" {
|
|
||||||
detail = reason
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(detail)
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
conn,
|
|
||||||
"HTTP/1.1 %d %s\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
|
|
||||||
code,
|
|
||||||
reason,
|
|
||||||
len(body),
|
|
||||||
)
|
|
||||||
_, _ = conn.Write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer int, chunkOpts chunkClientOptions, slots chan struct{}) {
|
|
||||||
defer func() {
|
|
||||||
<-slots
|
|
||||||
_ = conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
protocol.TuneTCP(conn)
|
|
||||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
|
|
||||||
|
|
||||||
header, extra, err := readHTTPHeaders(conn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
firstLine := strings.SplitN(string(header), "\r\n", 2)[0]
|
|
||||||
parts := strings.SplitN(firstLine, " ", 3)
|
|
||||||
|
|
||||||
if len(parts) != 3 {
|
|
||||||
writeHTTPError(conn, 400, "Bad Request", "invalid HTTP request line")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
method, target := parts[0], parts[1]
|
|
||||||
|
|
||||||
if strings.EqualFold(method, "CONNECT") {
|
|
||||||
host, port, err := parseHostPort(target, 443)
|
|
||||||
if err != nil {
|
|
||||||
writeHTTPError(conn, 400, "Bad Request", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote net.Conn
|
|
||||||
if transport == "chunk" {
|
|
||||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
|
||||||
} else {
|
|
||||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
writeHTTPError(conn, 502, "Bad Gateway", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer remote.Close()
|
|
||||||
|
|
||||||
_, _ = conn.Write([]byte(
|
|
||||||
"HTTP/1.1 200 Connection Established\r\n" +
|
|
||||||
"Proxy-Agent: dragontcp-proxy/2.0\r\n\r\n",
|
|
||||||
))
|
|
||||||
|
|
||||||
if len(extra) > 0 {
|
|
||||||
if transport == "xor" {
|
|
||||||
protocol.XorInPlace(extra)
|
|
||||||
}
|
|
||||||
if _, err := remote.Write(extra); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Time{})
|
|
||||||
if transport == "xor" {
|
|
||||||
protocol.RelayXOR(conn, remote)
|
|
||||||
} else {
|
|
||||||
// raw and chunk connections expose a normal plaintext net.Conn.
|
|
||||||
protocol.RelayRaw(conn, remote)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, rewritten, err := rewritePlainHTTPRequest(header)
|
|
||||||
if err != nil {
|
|
||||||
writeHTTPError(conn, 400, "Bad Request", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote net.Conn
|
|
||||||
if transport == "chunk" {
|
|
||||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
|
||||||
} else {
|
|
||||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
writeHTTPError(conn, 502, "Bad Gateway", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer remote.Close()
|
|
||||||
|
|
||||||
initial := make([]byte, 0, len(rewritten)+len(extra))
|
|
||||||
initial = append(initial, rewritten...)
|
|
||||||
initial = append(initial, extra...)
|
|
||||||
if transport == "xor" {
|
|
||||||
protocol.XorInPlace(initial)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := remote.Write(initial); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Time{})
|
|
||||||
if transport == "xor" {
|
|
||||||
protocol.RelayXOR(conn, remote)
|
|
||||||
} else {
|
|
||||||
protocol.RelayRaw(conn, remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
listenHost = flag.String("listen-host", "127.0.0.1", "local proxy listen host")
|
|
||||||
listenPort = flag.Int("listen-port", 8080, "local proxy listen port")
|
|
||||||
serverHost = flag.String("server-host", "", "remote DragonTCP server host")
|
|
||||||
serverPort = flag.Int("server-port", 53, "remote DragonTCP server port")
|
|
||||||
token = flag.String("token", "", "optional shared token")
|
|
||||||
maxConnections = flag.Int("max-connections", 20000, "max simultaneous proxy connections")
|
|
||||||
transport = flag.String("transport", "chunk", "transport: chunk (DragonTCP binary adaptive transport)")
|
|
||||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
|
||||||
chunkStart = flag.Int("chunk-start", 1048576, "initial adaptive chunk payload bytes")
|
|
||||||
chunkMin = flag.Int("chunk-min", 32, "minimum adaptive chunk payload bytes")
|
|
||||||
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (up to 1 MiB)")
|
|
||||||
chunkAdaptive = flag.Bool("chunk-adaptive", true, "automatically shrink on failures and grow after stable success")
|
|
||||||
chunkSuccesses = flag.Int("chunk-grow-after", 16, "successful data records required before increasing chunk size")
|
|
||||||
chunkAdaptLog = flag.Bool("chunk-adapt-log", true, "print adaptive chunk size changes")
|
|
||||||
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
|
|
||||||
chunkPollers = flag.Int("chunk-pollers", 1, "reserved compatibility setting; binary transport uses one download worker")
|
|
||||||
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "force reconnect after N logical requests; 0 = persistent/automatic")
|
|
||||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
|
||||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
|
||||||
)
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *serverHost == "" {
|
|
||||||
fmt.Fprintln(os.Stderr, "--server-host is required")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
*transport = strings.ToLower(*transport)
|
|
||||||
if *transport != "chunk" {
|
|
||||||
fmt.Fprintln(os.Stderr, "DragonTCP requires --transport chunk (binary adaptive TCP/53 transport)")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if *chunkSizeLegacy != 0 {
|
|
||||||
if *chunkSizeLegacy < 32 || *chunkSizeLegacy > protocol.MaxChunkPayload {
|
|
||||||
fmt.Fprintf(os.Stderr, "--chunk-size must be between 32 and %d\n", protocol.MaxChunkPayload)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
*chunkStart = *chunkSizeLegacy
|
|
||||||
*chunkMin = *chunkSizeLegacy
|
|
||||||
*chunkMax = *chunkSizeLegacy
|
|
||||||
*chunkAdaptive = false
|
|
||||||
}
|
|
||||||
if *chunkMin < 32 || *chunkMax > protocol.MaxChunkPayload || *chunkMin > *chunkStart || *chunkStart > *chunkMax {
|
|
||||||
fmt.Fprintf(os.Stderr, "require 32 <= --chunk-min <= --chunk-start <= --chunk-max <= %d\n", protocol.MaxChunkPayload)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if *chunkSuccesses < 1 {
|
|
||||||
fmt.Fprintln(os.Stderr, "--chunk-grow-after must be at least 1")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if *chunkPollers < 1 || *chunkPollers > 128 {
|
|
||||||
fmt.Fprintln(os.Stderr, "--chunk-pollers must be between 1 and 128")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if *chunkReconnect < 0 {
|
|
||||||
fmt.Fprintln(os.Stderr, "--chunk-reconnect-every must be 0 or greater")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
chunkOpts := chunkClientOptions{
|
|
||||||
startSize: *chunkStart,
|
|
||||||
minSize: *chunkMin,
|
|
||||||
maxSize: *chunkMax,
|
|
||||||
adaptive: *chunkAdaptive,
|
|
||||||
adaptSuccesses: *chunkSuccesses,
|
|
||||||
adaptLog: *chunkAdaptLog,
|
|
||||||
pollers: *chunkPollers,
|
|
||||||
reconnectEvery: *chunkReconnect,
|
|
||||||
pollDelay: *chunkPollDelay,
|
|
||||||
txnTimeout: *chunkTimeout,
|
|
||||||
tcpBuffer: *tcpBuffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
listenAddr := net.JoinHostPort(*listenHost, strconv.Itoa(*listenPort))
|
|
||||||
serverAddr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
fmt.Printf("local Go HTTP proxy listening on %s\n", listenAddr)
|
|
||||||
fmt.Printf("remote DragonTCP endpoint=%s\n", serverAddr)
|
|
||||||
fmt.Printf("max_connections=%d transport=%s tcp_buffer=%d\n", *maxConnections, *transport, *tcpBuffer)
|
|
||||||
if *transport == "chunk" {
|
|
||||||
fmt.Printf(
|
|
||||||
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d pollers=%d reconnect_every=%d timeout=%s\n",
|
|
||||||
*chunkAdaptive,
|
|
||||||
*chunkStart,
|
|
||||||
*chunkMin,
|
|
||||||
*chunkMax,
|
|
||||||
*chunkSuccesses,
|
|
||||||
*chunkPollers,
|
|
||||||
*chunkReconnect,
|
|
||||||
chunkTimeout.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
slots := make(chan struct{}, *maxConnections)
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := ln.Accept()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "accept:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case slots <- struct{}{}:
|
|
||||||
go handleLocal(conn, serverAddr, *token, *transport, *tcpBuffer, chunkOpts, slots)
|
|
||||||
default:
|
|
||||||
writeHTTPError(
|
|
||||||
conn,
|
|
||||||
503,
|
|
||||||
"Service Unavailable",
|
|
||||||
"proxy connection limit reached",
|
|
||||||
)
|
|
||||||
_ = conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,507 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"dragontcp/internal/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
type streamSession struct {
|
|
||||||
sid wire.SessionID
|
|
||||||
target net.Conn
|
|
||||||
targetName string
|
|
||||||
maxChunk int
|
|
||||||
maxBuffer int
|
|
||||||
debug *serverDebug
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
notify chan struct{}
|
|
||||||
buf []byte
|
|
||||||
base uint64
|
|
||||||
eof bool
|
|
||||||
closed bool
|
|
||||||
lastSeen time.Time
|
|
||||||
|
|
||||||
upMu sync.Mutex
|
|
||||||
expectedUp uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStreamSession(sid wire.SessionID, target net.Conn, targetName string, maxChunk, maxBuffer int, debug *serverDebug) *streamSession {
|
|
||||||
s := &streamSession{
|
|
||||||
sid: sid,
|
|
||||||
target: target,
|
|
||||||
targetName: targetName,
|
|
||||||
maxChunk: maxChunk,
|
|
||||||
maxBuffer: maxBuffer,
|
|
||||||
debug: debug,
|
|
||||||
notify: make(chan struct{}),
|
|
||||||
lastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
go s.readTarget()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) signalLocked() {
|
|
||||||
close(s.notify)
|
|
||||||
s.notify = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) touchLocked() { s.lastSeen = time.Now() }
|
|
||||||
|
|
||||||
func (s *streamSession) readTarget() {
|
|
||||||
tmp := make([]byte, 64*1024)
|
|
||||||
for {
|
|
||||||
n, err := s.target.Read(tmp)
|
|
||||||
if n > 0 {
|
|
||||||
data := append([]byte(nil), tmp[:n]...)
|
|
||||||
for len(data) > 0 {
|
|
||||||
s.mu.Lock()
|
|
||||||
for !s.closed && len(s.buf) >= s.maxBuffer {
|
|
||||||
ch := s.notify
|
|
||||||
s.mu.Unlock()
|
|
||||||
<-ch
|
|
||||||
s.mu.Lock()
|
|
||||||
}
|
|
||||||
if s.closed {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
room := s.maxBuffer - len(s.buf)
|
|
||||||
take := len(data)
|
|
||||||
if take > room {
|
|
||||||
take = room
|
|
||||||
}
|
|
||||||
s.buf = append(s.buf, data[:take]...)
|
|
||||||
data = data[take:]
|
|
||||||
s.touchLocked()
|
|
||||||
s.signalLocked()
|
|
||||||
s.mu.Unlock()
|
|
||||||
if s.debug != nil && s.debug.enabled {
|
|
||||||
s.debug.bytesDown.Add(uint64(take))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.mu.Lock()
|
|
||||||
if !s.closed {
|
|
||||||
s.eof = true
|
|
||||||
s.touchLocked()
|
|
||||||
s.signalLocked()
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) ackLocked(offset uint64) {
|
|
||||||
if offset <= s.base {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
end := s.base + uint64(len(s.buf))
|
|
||||||
if offset > end {
|
|
||||||
offset = end
|
|
||||||
}
|
|
||||||
drop := int(offset - s.base)
|
|
||||||
if drop <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.buf = s.buf[drop:]
|
|
||||||
s.base = offset
|
|
||||||
if len(s.buf) == 0 {
|
|
||||||
s.buf = nil
|
|
||||||
} else if cap(s.buf) > 4*len(s.buf) && cap(s.buf) > 1024*1024 {
|
|
||||||
compact := append([]byte(nil), s.buf...)
|
|
||||||
s.buf = compact
|
|
||||||
}
|
|
||||||
s.signalLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) ack(offset uint64) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.ackLocked(offset)
|
|
||||||
s.touchLocked()
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) readAt(offset uint64, limit int, wait time.Duration) ([]byte, byte, error) {
|
|
||||||
if limit < 1 || limit > s.maxChunk {
|
|
||||||
return nil, wire.StatusError, fmt.Errorf("invalid download limit %d", limit)
|
|
||||||
}
|
|
||||||
deadline := time.Now().Add(wait)
|
|
||||||
firstDataAt := time.Time{}
|
|
||||||
|
|
||||||
for {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.touchLocked()
|
|
||||||
if offset < s.base {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil, wire.StatusError, fmt.Errorf("download offset %d was already acknowledged (base=%d)", offset, s.base)
|
|
||||||
}
|
|
||||||
rel64 := offset - s.base
|
|
||||||
if rel64 <= uint64(len(s.buf)) {
|
|
||||||
rel := int(rel64)
|
|
||||||
available := len(s.buf) - rel
|
|
||||||
if available > 0 {
|
|
||||||
if firstDataAt.IsZero() {
|
|
||||||
firstDataAt = time.Now()
|
|
||||||
}
|
|
||||||
// Coalesce tiny target reads briefly. This prevents a 1-2 byte
|
|
||||||
// producer read from becoming a permanent tiny tunnel record.
|
|
||||||
if available < limit && !s.eof && wait > 0 && time.Since(firstDataAt) < 2*time.Millisecond {
|
|
||||||
ch := s.notify
|
|
||||||
s.mu.Unlock()
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
case <-time.After(2 * time.Millisecond):
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n := available
|
|
||||||
if n > limit {
|
|
||||||
n = limit
|
|
||||||
}
|
|
||||||
out := append([]byte(nil), s.buf[rel:rel+n]...)
|
|
||||||
s.mu.Unlock()
|
|
||||||
return out, wire.StatusData, nil
|
|
||||||
}
|
|
||||||
if s.eof || s.closed {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil, wire.StatusEOF, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil, wire.StatusError, fmt.Errorf("download offset %d is beyond buffered stream end %d", offset, s.base+uint64(len(s.buf)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if wait <= 0 || time.Now().After(deadline) {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil, wire.StatusWait, nil
|
|
||||||
}
|
|
||||||
ch := s.notify
|
|
||||||
remaining := time.Until(deadline)
|
|
||||||
s.mu.Unlock()
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
case <-time.After(remaining):
|
|
||||||
return nil, wire.StatusWait, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) upload(offset uint64, data []byte) error {
|
|
||||||
if len(data) == 0 || len(data) > s.maxChunk {
|
|
||||||
return fmt.Errorf("invalid upload size %d", len(data))
|
|
||||||
}
|
|
||||||
s.upMu.Lock()
|
|
||||||
defer s.upMu.Unlock()
|
|
||||||
|
|
||||||
if offset < s.expectedUp {
|
|
||||||
// Idempotent retry after a lost ACK.
|
|
||||||
if offset+uint64(len(data)) <= s.expectedUp {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("overlapping upload retry at %d", offset)
|
|
||||||
}
|
|
||||||
if offset != s.expectedUp {
|
|
||||||
return fmt.Errorf("upload gap: got %d expected %d", offset, s.expectedUp)
|
|
||||||
}
|
|
||||||
if _, err := s.target.Write(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.expectedUp += uint64(len(data))
|
|
||||||
s.mu.Lock()
|
|
||||||
s.touchLocked()
|
|
||||||
s.mu.Unlock()
|
|
||||||
if s.debug != nil && s.debug.enabled {
|
|
||||||
s.debug.bytesUp.Add(uint64(len(data)))
|
|
||||||
s.debug.pushRecords.Add(1)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *streamSession) close() {
|
|
||||||
s.mu.Lock()
|
|
||||||
if s.closed {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.closed = true
|
|
||||||
s.signalLocked()
|
|
||||||
s.mu.Unlock()
|
|
||||||
_ = s.target.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamManager struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
sessions map[string]*streamSession
|
|
||||||
timeout time.Duration
|
|
||||||
debug *serverDebug
|
|
||||||
}
|
|
||||||
|
|
||||||
func sidKey(sid wire.SessionID) string { return string(sid[:]) }
|
|
||||||
|
|
||||||
func newStreamManager(timeout time.Duration, debug *serverDebug) *streamManager {
|
|
||||||
m := &streamManager{sessions: make(map[string]*streamSession), timeout: timeout, debug: debug}
|
|
||||||
go m.cleanupLoop()
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *streamManager) get(sid wire.SessionID) *streamSession {
|
|
||||||
m.mu.RLock()
|
|
||||||
s := m.sessions[sidKey(sid)]
|
|
||||||
m.mu.RUnlock()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *streamManager) addOrGet(sid wire.SessionID, s *streamSession) (*streamSession, bool) {
|
|
||||||
key := sidKey(sid)
|
|
||||||
m.mu.Lock()
|
|
||||||
if old := m.sessions[key]; old != nil {
|
|
||||||
m.mu.Unlock()
|
|
||||||
s.close()
|
|
||||||
return old, false
|
|
||||||
}
|
|
||||||
m.sessions[key] = s
|
|
||||||
m.mu.Unlock()
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *streamManager) remove(sid wire.SessionID) {
|
|
||||||
key := sidKey(sid)
|
|
||||||
m.mu.Lock()
|
|
||||||
s := m.sessions[key]
|
|
||||||
delete(m.sessions, key)
|
|
||||||
m.mu.Unlock()
|
|
||||||
if s != nil {
|
|
||||||
s.close()
|
|
||||||
if m.debug != nil && m.debug.enabled {
|
|
||||||
m.debug.sessionsClosed.Add(1)
|
|
||||||
m.debug.activeSessions.Add(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *streamManager) count() int { m.mu.RLock(); n := len(m.sessions); m.mu.RUnlock(); return n }
|
|
||||||
|
|
||||||
func (m *streamManager) cleanupLoop() {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
cutoff := time.Now().Add(-m.timeout)
|
|
||||||
var stale []wire.SessionID
|
|
||||||
m.mu.RLock()
|
|
||||||
for _, s := range m.sessions {
|
|
||||||
s.mu.Lock()
|
|
||||||
last := s.lastSeen
|
|
||||||
closed := s.closed
|
|
||||||
sid := s.sid
|
|
||||||
s.mu.Unlock()
|
|
||||||
if closed || last.Before(cutoff) {
|
|
||||||
stale = append(stale, sid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.mu.RUnlock()
|
|
||||||
for _, sid := range stale {
|
|
||||||
m.remove(sid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseProbe(payload []byte) (kind byte, value int, token string, err error) {
|
|
||||||
if len(payload) < 11 || !bytes.Equal(payload[:4], wire.ProbeMagic[:]) {
|
|
||||||
return 0, 0, "", fmt.Errorf("bad probe payload")
|
|
||||||
}
|
|
||||||
kind = payload[4]
|
|
||||||
tl := int(binary.BigEndian.Uint16(payload[5:7]))
|
|
||||||
value = int(binary.BigEndian.Uint32(payload[7:11]))
|
|
||||||
if 11+tl > len(payload) {
|
|
||||||
return 0, 0, "", fmt.Errorf("bad probe token length")
|
|
||||||
}
|
|
||||||
token = string(payload[11 : 11+tl])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func probePattern(n int) []byte {
|
|
||||||
out := make([]byte, n)
|
|
||||||
for i := range out {
|
|
||||||
out[i] = byte((i*31 + 17) & 0xff)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOpen(payload []byte) (token, host string, port int, err error) {
|
|
||||||
if len(payload) < 6 {
|
|
||||||
return "", "", 0, fmt.Errorf("bad OPEN payload")
|
|
||||||
}
|
|
||||||
tl := int(binary.BigEndian.Uint16(payload[0:2]))
|
|
||||||
hl := int(binary.BigEndian.Uint16(payload[2:4]))
|
|
||||||
port = int(binary.BigEndian.Uint16(payload[4:6]))
|
|
||||||
if port < 1 || 6+tl+hl != len(payload) {
|
|
||||||
return "", "", 0, fmt.Errorf("bad OPEN lengths")
|
|
||||||
}
|
|
||||||
token = string(payload[6 : 6+tl])
|
|
||||||
host = string(payload[6+tl:])
|
|
||||||
if host == "" {
|
|
||||||
return "", "", 0, fmt.Errorf("empty target host")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func processWireRequest(conn net.Conn, req wire.Request, token string, allowPrivate bool, cache *dnsCache, tcpBuffer int, manager *streamManager, maxChunk, maxBuffer int, pollWait time.Duration, debug *serverDebug) error {
|
|
||||||
switch req.Mode {
|
|
||||||
case wire.ModeProbe:
|
|
||||||
kind, value, supplied, err := parseProbe(req.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte(err.Error()))
|
|
||||||
}
|
|
||||||
if !tokenEqual(supplied, token) {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("authentication failed"))
|
|
||||||
}
|
|
||||||
switch kind {
|
|
||||||
case wire.ProbeUpload:
|
|
||||||
if len(req.Payload) > maxChunk {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("probe too large"))
|
|
||||||
}
|
|
||||||
return wire.WriteResponse(conn, wire.StatusOK, nil)
|
|
||||||
case wire.ProbeDownload:
|
|
||||||
if value < 1 || value > maxChunk {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("probe too large"))
|
|
||||||
}
|
|
||||||
return wire.WriteMaskedResponse(conn, wire.StatusData, probePattern(value), req.Session, wire.ModeProbe, req.Seq)
|
|
||||||
case wire.ProbeKeepalive:
|
|
||||||
return wire.WriteResponse(conn, wire.StatusOK, nil)
|
|
||||||
case wire.ProbeBatch:
|
|
||||||
count := value
|
|
||||||
if count < 1 {
|
|
||||||
count = 1
|
|
||||||
}
|
|
||||||
if count > 16 {
|
|
||||||
count = 16
|
|
||||||
}
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
data := probePattern(32)
|
|
||||||
if err := wire.WriteMaskedResponse(conn, wire.StatusData, data, req.Session, wire.ModeProbe, req.Seq+uint64(i)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("unknown probe kind"))
|
|
||||||
}
|
|
||||||
|
|
||||||
case wire.ModeOpen:
|
|
||||||
supplied, host, port, err := parseOpen(req.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte(err.Error()))
|
|
||||||
}
|
|
||||||
if !tokenEqual(supplied, token) {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("authentication failed"))
|
|
||||||
}
|
|
||||||
if old := manager.get(req.Session); old != nil {
|
|
||||||
body := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(body, uint32(maxChunk))
|
|
||||||
return wire.WriteMaskedResponse(conn, wire.StatusOK, body, req.Session, wire.ModeOpen, req.Seq)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
target, err := dialTarget(ctx, host, port, allowPrivate, cache, tcpBuffer)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte(err.Error()))
|
|
||||||
}
|
|
||||||
session := newStreamSession(req.Session, target, fmt.Sprintf("%s:%d", host, port), maxChunk, maxBuffer, debug)
|
|
||||||
_, created := manager.addOrGet(req.Session, session)
|
|
||||||
if created && debug != nil && debug.enabled {
|
|
||||||
debug.sessionsOpened.Add(1)
|
|
||||||
debug.activeSessions.Add(1)
|
|
||||||
debug.logf("SESSION OPEN sid=%x target=%s:%d active_sessions=%d", req.Session[:4], host, port, manager.count())
|
|
||||||
}
|
|
||||||
body := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(body, uint32(maxChunk))
|
|
||||||
return wire.WriteMaskedResponse(conn, wire.StatusOK, body, req.Session, wire.ModeOpen, req.Seq)
|
|
||||||
|
|
||||||
case wire.ModeUpload:
|
|
||||||
s := manager.get(req.Session)
|
|
||||||
if s == nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("unknown session"))
|
|
||||||
}
|
|
||||||
if len(req.Payload) > maxChunk {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("upload too large"))
|
|
||||||
}
|
|
||||||
if err := s.upload(req.Seq, req.Payload); err != nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte(err.Error()))
|
|
||||||
}
|
|
||||||
return wire.WriteResponse(conn, wire.StatusOK, nil)
|
|
||||||
|
|
||||||
case wire.ModeDownload:
|
|
||||||
s := manager.get(req.Session)
|
|
||||||
if s == nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("unknown session"))
|
|
||||||
}
|
|
||||||
if len(req.Payload) != 14 {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("bad download request"))
|
|
||||||
}
|
|
||||||
ack := binary.BigEndian.Uint64(req.Payload[0:8])
|
|
||||||
limit := int(binary.BigEndian.Uint32(req.Payload[8:12]))
|
|
||||||
count := int(binary.BigEndian.Uint16(req.Payload[12:14]))
|
|
||||||
if limit < 1 {
|
|
||||||
limit = 1
|
|
||||||
}
|
|
||||||
if limit > maxChunk {
|
|
||||||
limit = maxChunk
|
|
||||||
}
|
|
||||||
if count < 1 {
|
|
||||||
count = 1
|
|
||||||
}
|
|
||||||
if count > 256 {
|
|
||||||
count = 256
|
|
||||||
}
|
|
||||||
s.ack(ack)
|
|
||||||
offset := req.Seq
|
|
||||||
if debug != nil && debug.enabled {
|
|
||||||
debug.pullRequests.Add(1)
|
|
||||||
}
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
wait := time.Duration(0)
|
|
||||||
if i == 0 {
|
|
||||||
wait = pollWait
|
|
||||||
}
|
|
||||||
data, status, err := s.readAt(offset, limit, wait)
|
|
||||||
if err != nil {
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte(err.Error()))
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case wire.StatusData:
|
|
||||||
if debug != nil && debug.enabled {
|
|
||||||
debug.dataRecords.Add(1)
|
|
||||||
}
|
|
||||||
if err := wire.WriteMaskedResponse(conn, wire.StatusData, data, req.Session, wire.ModeDownload, offset); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offset += uint64(len(data))
|
|
||||||
case wire.StatusWait:
|
|
||||||
if debug != nil && debug.enabled {
|
|
||||||
debug.waitRecords.Add(1)
|
|
||||||
}
|
|
||||||
return wire.WriteResponse(conn, wire.StatusWait, nil)
|
|
||||||
case wire.StatusEOF:
|
|
||||||
return wire.WriteResponse(conn, wire.StatusEOF, nil)
|
|
||||||
default:
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("invalid session read status"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case wire.ModeClose:
|
|
||||||
manager.remove(req.Session)
|
|
||||||
return wire.WriteResponse(conn, wire.StatusOK, nil)
|
|
||||||
default:
|
|
||||||
return wire.WriteResponse(conn, wire.StatusError, []byte("unknown mode"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseOpenAllowsEmptyToken(t *testing.T) {
|
|
||||||
host := "example.com"
|
|
||||||
p := make([]byte, 6+len(host))
|
|
||||||
binary.BigEndian.PutUint16(p[0:2], 0)
|
|
||||||
binary.BigEndian.PutUint16(p[2:4], uint16(len(host)))
|
|
||||||
binary.BigEndian.PutUint16(p[4:6], 443)
|
|
||||||
copy(p[6:], host)
|
|
||||||
token, gotHost, port, err := parseOpen(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if token != "" || gotHost != host || port != 443 {
|
|
||||||
t.Fatalf("got token=%q host=%q port=%d", token, gotHost, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serverDebug struct {
|
|
||||||
enabled bool
|
|
||||||
chunks bool
|
|
||||||
statsEvery time.Duration
|
|
||||||
started time.Time
|
|
||||||
|
|
||||||
sessionsOpened atomic.Uint64
|
|
||||||
sessionsClosed atomic.Uint64
|
|
||||||
activeSessions atomic.Int64
|
|
||||||
bytesUp atomic.Uint64
|
|
||||||
bytesDown atomic.Uint64
|
|
||||||
pushRecords atomic.Uint64
|
|
||||||
pullRequests atomic.Uint64
|
|
||||||
dataRecords atomic.Uint64
|
|
||||||
waitRecords atomic.Uint64
|
|
||||||
errors atomic.Uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServerDebug(enabled, chunks bool, statsEvery time.Duration) *serverDebug {
|
|
||||||
d := &serverDebug{
|
|
||||||
enabled: enabled || chunks,
|
|
||||||
chunks: chunks,
|
|
||||||
statsEvery: statsEvery,
|
|
||||||
started: time.Now(),
|
|
||||||
}
|
|
||||||
if d.enabled && d.statsEvery > 0 {
|
|
||||||
go d.statsLoop()
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *serverDebug) logf(format string, args ...any) {
|
|
||||||
if d == nil || !d.enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "%s [DEBUG] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *serverDebug) chunkf(format string, args ...any) {
|
|
||||||
if d == nil || !d.chunks {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "%s [CHUNK] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *serverDebug) errorf(format string, args ...any) {
|
|
||||||
if d == nil || !d.enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.errors.Add(1)
|
|
||||||
fmt.Fprintf(os.Stderr, "%s [ERROR] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *serverDebug) statsLoop() {
|
|
||||||
ticker := time.NewTicker(d.statsEvery)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
d.logf(
|
|
||||||
"STATS uptime=%s active_connections=%d active_sessions=%d sessions_opened=%d sessions_closed=%d bytes_up=%d bytes_down=%d push_records=%d pull_requests=%d data_records=%d waits=%d errors=%d",
|
|
||||||
time.Since(d.started).Round(time.Second),
|
|
||||||
atomic.LoadInt64(&active),
|
|
||||||
d.activeSessions.Load(),
|
|
||||||
d.sessionsOpened.Load(),
|
|
||||||
d.sessionsClosed.Load(),
|
|
||||||
d.bytesUp.Load(),
|
|
||||||
d.bytesDown.Load(),
|
|
||||||
d.pushRecords.Load(),
|
|
||||||
d.pullRequests.Load(),
|
|
||||||
d.dataRecords.Load(),
|
|
||||||
d.waitRecords.Load(),
|
|
||||||
d.errors.Load(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/subtle"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"dragontcp/internal/protocol"
|
|
||||||
"dragontcp/internal/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
var active int64
|
|
||||||
|
|
||||||
type dnsEntry struct {
|
|
||||||
ips []netip.Addr
|
|
||||||
expires time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type dnsCache struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
entries map[string]dnsEntry
|
|
||||||
ttl time.Duration
|
|
||||||
max int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDNSCache(ttl time.Duration, max int) *dnsCache {
|
|
||||||
return &dnsCache{
|
|
||||||
entries: make(map[string]dnsEntry),
|
|
||||||
ttl: ttl,
|
|
||||||
max: max,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dnsCache) resolve(ctx context.Context, host string) ([]netip.Addr, error) {
|
|
||||||
if ip, err := netip.ParseAddr(host); err == nil {
|
|
||||||
return []netip.Addr{ip}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
c.mu.RLock()
|
|
||||||
entry, ok := c.entries[host]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
if ok && now.Before(entry.expires) {
|
|
||||||
return entry.ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
if len(c.entries) >= c.max {
|
|
||||||
// Simple bounded reset keeps the hot cache cheap and prevents growth.
|
|
||||||
c.entries = make(map[string]dnsEntry, c.max)
|
|
||||||
}
|
|
||||||
c.entries[host] = dnsEntry{ips: ips, expires: now.Add(c.ttl)}
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenEqual(a, b string) bool {
|
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockedSpecial = []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("0.0.0.0/8"),
|
|
||||||
netip.MustParsePrefix("100.64.0.0/10"),
|
|
||||||
netip.MustParsePrefix("192.0.0.0/24"),
|
|
||||||
netip.MustParsePrefix("192.0.2.0/24"),
|
|
||||||
netip.MustParsePrefix("198.18.0.0/15"),
|
|
||||||
netip.MustParsePrefix("198.51.100.0/24"),
|
|
||||||
netip.MustParsePrefix("203.0.113.0/24"),
|
|
||||||
netip.MustParsePrefix("240.0.0.0/4"),
|
|
||||||
netip.MustParsePrefix("2001:db8::/32"),
|
|
||||||
}
|
|
||||||
|
|
||||||
func addressAllowed(addr netip.Addr, allowPrivate bool) bool {
|
|
||||||
if addr.IsUnspecified() || addr.IsMulticast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowPrivate {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addr.IsGlobalUnicast() ||
|
|
||||||
addr.IsPrivate() ||
|
|
||||||
addr.IsLoopback() ||
|
|
||||||
addr.IsLinkLocalUnicast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prefix := range blockedSpecial {
|
|
||||||
if prefix.Contains(addr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialTarget(ctx context.Context, host string, port int, allowPrivate bool, cache *dnsCache, tcpBuffer int) (net.Conn, error) {
|
|
||||||
ips, err := cache.resolve(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
var blocked []string
|
|
||||||
|
|
||||||
d := net.Dialer{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
if !addressAllowed(ip, allowPrivate) {
|
|
||||||
blocked = append(blocked, ip.String())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := net.JoinHostPort(ip.String(), strconv.Itoa(port))
|
|
||||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
||||||
if err == nil {
|
|
||||||
protocol.TuneTCP(conn)
|
|
||||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastErr != nil {
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
if len(blocked) > 0 {
|
|
||||||
return nil, fmt.Errorf("target resolves only to blocked addresses: %s", strings.Join(blocked, ","))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("no usable target address")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(
|
|
||||||
conn net.Conn,
|
|
||||||
token string,
|
|
||||||
allowPrivate bool,
|
|
||||||
cache *dnsCache,
|
|
||||||
tcpBuffer int,
|
|
||||||
slots chan struct{},
|
|
||||||
manager *streamManager,
|
|
||||||
chunkMax int,
|
|
||||||
bufferBytes int,
|
|
||||||
chunkPollWait time.Duration,
|
|
||||||
debug *serverDebug,
|
|
||||||
) {
|
|
||||||
defer func() {
|
|
||||||
<-slots
|
|
||||||
atomic.AddInt64(&active, -1)
|
|
||||||
_ = conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
protocol.TuneTCP(conn)
|
|
||||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
|
||||||
|
|
||||||
for {
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
||||||
req, err := wire.ReadRequest(conn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := processWireRequest(
|
|
||||||
conn,
|
|
||||||
req,
|
|
||||||
token,
|
|
||||||
allowPrivate,
|
|
||||||
cache,
|
|
||||||
tcpBuffer,
|
|
||||||
manager,
|
|
||||||
chunkMax,
|
|
||||||
bufferBytes,
|
|
||||||
chunkPollWait,
|
|
||||||
debug,
|
|
||||||
); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
host = flag.String("host", "0.0.0.0", "listen host")
|
|
||||||
port = flag.Int("port", 53, "listen port")
|
|
||||||
token = flag.String("token", "", "optional shared token")
|
|
||||||
maxConnections = flag.Int("max-connections", 20000, "max simultaneous tunnels")
|
|
||||||
allowPrivate = flag.Bool("allow-private", false, "allow private/loopback targets")
|
|
||||||
dnsCacheTTL = flag.Duration("dns-cache-ttl", 30*time.Second, "server DNS cache TTL")
|
|
||||||
dnsCacheSize = flag.Int("dns-cache-size", 4096, "maximum cached DNS hostnames")
|
|
||||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
|
||||||
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (32 bytes to 1 MiB)")
|
|
||||||
chunkBuffered = flag.Int("chunk-buffered", 256, "compatibility buffer units; 256 = about 16 MiB per active session")
|
|
||||||
chunkPollWait = flag.Duration("chunk-poll-wait", 200*time.Millisecond, "server long-poll wait for chunk data")
|
|
||||||
sessionTimeout = flag.Duration("chunk-session-timeout", 2*time.Minute, "idle chunk session timeout")
|
|
||||||
debugEnabled = flag.Bool("debug", false, "log session/connect/errors and periodic statistics")
|
|
||||||
debugChunks = flag.Bool("debug-chunks", false, "log every chunk protocol record; very verbose")
|
|
||||||
debugStats = flag.Duration("debug-stats-interval", 5*time.Second, "periodic debug statistics interval; 0 disables")
|
|
||||||
)
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *chunkMax < 32 || *chunkMax > protocol.MaxChunkPayload {
|
|
||||||
fmt.Fprintf(os.Stderr, "--chunk-max must be between 32 and %d\n", protocol.MaxChunkPayload)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if *chunkBuffered < 8 {
|
|
||||||
fmt.Fprintln(os.Stderr, "--chunk-buffered must be at least 8")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenAddr := net.JoinHostPort(*host, strconv.Itoa(*port))
|
|
||||||
ln, err := net.Listen("tcp", listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
fmt.Printf("DragonTCP Go server listening on %s\n", listenAddr)
|
|
||||||
fmt.Printf("max_connections=%d tcp_buffer=%d\n", *maxConnections, *tcpBuffer)
|
|
||||||
|
|
||||||
slots := make(chan struct{}, *maxConnections)
|
|
||||||
cache := newDNSCache(*dnsCacheTTL, *dnsCacheSize)
|
|
||||||
debug := newServerDebug(*debugEnabled, *debugChunks, *debugStats)
|
|
||||||
bufferBytes := *chunkBuffered * 65536
|
|
||||||
if bufferBytes < 1024*1024 {
|
|
||||||
bufferBytes = 1024 * 1024
|
|
||||||
}
|
|
||||||
if bufferBytes > 64*1024*1024 {
|
|
||||||
bufferBytes = 64 * 1024 * 1024
|
|
||||||
}
|
|
||||||
manager := newStreamManager(*sessionTimeout, debug)
|
|
||||||
fmt.Printf("binary_transport=true chunk_max=%d buffer_bytes=%d poll_wait=%s\n", *chunkMax, bufferBytes, chunkPollWait.String())
|
|
||||||
if debug.enabled {
|
|
||||||
fmt.Printf("debug=true debug_chunks=%t stats_interval=%s\n", debug.chunks, debug.statsEvery)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := ln.Accept()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "accept:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case slots <- struct{}{}:
|
|
||||||
atomic.AddInt64(&active, 1)
|
|
||||||
if debug.enabled {
|
|
||||||
debug.logf("ACCEPT peer=%v active_connections=%d", conn.RemoteAddr(), atomic.LoadInt64(&active))
|
|
||||||
}
|
|
||||||
go handle(
|
|
||||||
conn,
|
|
||||||
*token,
|
|
||||||
*allowPrivate,
|
|
||||||
cache,
|
|
||||||
*tcpBuffer,
|
|
||||||
slots,
|
|
||||||
manager,
|
|
||||||
*chunkMax,
|
|
||||||
bufferBytes,
|
|
||||||
*chunkPollWait,
|
|
||||||
debug,
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
if debug.enabled {
|
|
||||||
debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
_ = conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
module dragontcp
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package protocol
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
XORKey byte = 0xAD
|
|
||||||
|
|
||||||
// MaxChunkPayload is the hard application-record payload ceiling.
|
|
||||||
// The adaptive chunk protocol may use any size from 32 bytes through 1 MiB.
|
|
||||||
MaxChunkPayload = 1024 * 1024
|
|
||||||
|
|
||||||
// Framed CPUSH/DATA messages include text metadata in addition to chunk
|
|
||||||
// bytes, so keep the frame ceiling comfortably above MaxChunkPayload.
|
|
||||||
MaxHandshake = 2 * 1024 * 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// 64 KiB balances throughput with memory use at high connection counts.
|
|
||||||
var BufferPool = sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
b := make([]byte, 64*1024)
|
|
||||||
return &b
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
|
|
||||||
var header [14]byte
|
|
||||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
|
||||||
return 0, 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if header[0] != 'U' || header[1] != 'P' {
|
|
||||||
return 0, 0, nil, errors.New("bad request magic")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID := binary.BigEndian.Uint32(header[2:6])
|
|
||||||
reserved := binary.BigEndian.Uint32(header[6:10])
|
|
||||||
length := binary.BigEndian.Uint32(header[10:14])
|
|
||||||
|
|
||||||
if length > MaxHandshake {
|
|
||||||
return 0, 0, nil, errors.New("handshake payload too large")
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := make([]byte, int(length))
|
|
||||||
if _, err := io.ReadFull(r, payload); err != nil {
|
|
||||||
return 0, 0, nil, err
|
|
||||||
}
|
|
||||||
XorInPlace(payload)
|
|
||||||
|
|
||||||
return requestID, reserved, payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
|
|
||||||
if len(payload) > MaxHandshake {
|
|
||||||
return errors.New("request frame payload too large")
|
|
||||||
}
|
|
||||||
packet := make([]byte, 14+len(payload))
|
|
||||||
packet[0], packet[1] = 'U', 'P'
|
|
||||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
|
||||||
binary.BigEndian.PutUint32(packet[6:10], 0)
|
|
||||||
binary.BigEndian.PutUint32(packet[10:14], uint32(len(payload)))
|
|
||||||
copy(packet[14:], payload)
|
|
||||||
XorInPlace(packet[14:])
|
|
||||||
return writeAll(w, packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
|
|
||||||
var header [10]byte
|
|
||||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if header[0] != 'O' || header[1] != 'K' {
|
|
||||||
return 0, nil, fmt.Errorf("bad response magic: %q", header[:2])
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID := binary.BigEndian.Uint32(header[2:6])
|
|
||||||
length := binary.BigEndian.Uint32(header[6:10])
|
|
||||||
|
|
||||||
if length > MaxHandshake {
|
|
||||||
return 0, nil, errors.New("handshake response too large")
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := make([]byte, int(length))
|
|
||||||
if _, err := io.ReadFull(r, payload); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
XorInPlace(payload)
|
|
||||||
|
|
||||||
return requestID, payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteResponseFrame(w io.Writer, requestID uint32, payload []byte) error {
|
|
||||||
if len(payload) > MaxHandshake {
|
|
||||||
return errors.New("response frame payload too large")
|
|
||||||
}
|
|
||||||
packet := make([]byte, 10+len(payload))
|
|
||||||
packet[0], packet[1] = 'O', 'K'
|
|
||||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
|
||||||
binary.BigEndian.PutUint32(packet[6:10], uint32(len(payload)))
|
|
||||||
copy(packet[10:], payload)
|
|
||||||
XorInPlace(packet[10:])
|
|
||||||
return writeAll(w, packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAll(w io.Writer, b []byte) error {
|
|
||||||
for len(b) > 0 {
|
|
||||||
n, err := w.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b = b[n:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyXOR(dst net.Conn, src net.Conn) error {
|
|
||||||
ptr := BufferPool.Get().(*[]byte)
|
|
||||||
buf := *ptr
|
|
||||||
defer BufferPool.Put(ptr)
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := src.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
chunk := buf[:n]
|
|
||||||
XorInPlace(chunk)
|
|
||||||
|
|
||||||
if err2 := writeAll(dst, chunk); err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
// No restore pass is needed. The next Read overwrites these bytes.
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func relayPair(a, b net.Conn, copier func(net.Conn, net.Conn) error) {
|
|
||||||
done := make(chan struct{}, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = copier(b, a)
|
|
||||||
if cw, ok := b.(interface{ CloseWrite() error }); ok {
|
|
||||||
_ = cw.CloseWrite()
|
|
||||||
}
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = copier(a, b)
|
|
||||||
if cw, ok := a.(interface{ CloseWrite() error }); ok {
|
|
||||||
_ = cw.CloseWrite()
|
|
||||||
}
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Preserve normal TCP half-close semantics. The old implementation set a
|
|
||||||
// 2-second deadline on both connections after the first copy direction
|
|
||||||
// ended, which truncated slow or large responses. Wait for the remaining
|
|
||||||
// direction to drain naturally instead.
|
|
||||||
<-done
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
func RelayXOR(a, b net.Conn) {
|
|
||||||
relayPair(a, b, CopyXOR)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelayRaw allows Go/Linux to use the optimized TCP io.Copy path. On Linux,
|
|
||||||
// TCP-to-TCP copies can use splice, eliminating the userspace XOR/copy loop.
|
|
||||||
func RelayRaw(a, b net.Conn) {
|
|
||||||
relayPair(a, b, func(dst, src net.Conn) error {
|
|
||||||
_, err := io.Copy(dst, src)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TuneTCP(conn net.Conn) {
|
|
||||||
if tcp, ok := conn.(*net.TCPConn); ok {
|
|
||||||
_ = tcp.SetNoDelay(true)
|
|
||||||
_ = tcp.SetKeepAlive(true)
|
|
||||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TuneTCPBuffer optionally requests larger kernel socket buffers. A value <= 0
|
|
||||||
// leaves Linux/Android autotuning untouched, which is the recommended default
|
|
||||||
// for large connection counts. For a small number of high-BDP mobile links,
|
|
||||||
// values such as 1048576 or 4194304 can improve throughput.
|
|
||||||
func TuneTCPBuffer(conn net.Conn, size int) {
|
|
||||||
if size <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tcp, ok := conn.(*net.TCPConn); ok {
|
|
||||||
_ = tcp.SetReadBuffer(size)
|
|
||||||
_ = tcp.SetWriteBuffer(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
//go:build arm || 386
|
|
||||||
|
|
||||||
package protocol
|
|
||||||
|
|
||||||
import "unsafe"
|
|
||||||
|
|
||||||
const xorWordMask32 uint32 = 0xADADADAD
|
|
||||||
|
|
||||||
// XorInPlace is the 32-bit optimized path used by ARMv7/386 builds.
|
|
||||||
// It aligns once, then processes 32 bytes per iteration with native uint32
|
|
||||||
// operations instead of a byte-at-a-time loop.
|
|
||||||
func XorInPlace(b []byte) {
|
|
||||||
n := len(b)
|
|
||||||
if n == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for i < n && (uintptr(unsafe.Pointer(&b[i]))&3) != 0 {
|
|
||||||
b[i] ^= XORKey
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i+32 <= n; i += 32 {
|
|
||||||
p := unsafe.Pointer(&b[i])
|
|
||||||
*(*uint32)(unsafe.Add(p, 0)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 4)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 8)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 12)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 16)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 20)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 24)) ^= xorWordMask32
|
|
||||||
*(*uint32)(unsafe.Add(p, 28)) ^= xorWordMask32
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i+4 <= n; i += 4 {
|
|
||||||
p := (*uint32)(unsafe.Pointer(&b[i]))
|
|
||||||
*p ^= xorWordMask32
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i < n; i++ {
|
|
||||||
b[i] ^= XORKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
//go:build amd64 || arm64
|
|
||||||
|
|
||||||
package protocol
|
|
||||||
|
|
||||||
import "unsafe"
|
|
||||||
|
|
||||||
const xorWordMask uint64 = 0xADADADADADADADAD
|
|
||||||
|
|
||||||
// XorInPlace is optimized for 64-bit targets (amd64/arm64).
|
|
||||||
//
|
|
||||||
// It aligns the input once, then XORs 64 bytes per loop iteration using
|
|
||||||
// eight native 64-bit operations. This removes the encoding/binary call
|
|
||||||
// overhead from the hot relay path and lets the compiler generate a tight
|
|
||||||
// load/xor/store loop.
|
|
||||||
func XorInPlace(b []byte) {
|
|
||||||
n := len(b)
|
|
||||||
if n == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
// Align the pointer for native uint64 accesses. This is normally already
|
|
||||||
// aligned for pooled relay buffers, but also makes this safe for subslices.
|
|
||||||
for i < n && (uintptr(unsafe.Pointer(&b[i]))&7) != 0 {
|
|
||||||
b[i] ^= XORKey
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i+64 <= n; i += 64 {
|
|
||||||
p := unsafe.Pointer(&b[i])
|
|
||||||
*(*uint64)(unsafe.Add(p, 0)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 8)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 16)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 24)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 32)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 40)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 48)) ^= xorWordMask
|
|
||||||
*(*uint64)(unsafe.Add(p, 56)) ^= xorWordMask
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i+8 <= n; i += 8 {
|
|
||||||
p := (*uint64)(unsafe.Pointer(&b[i]))
|
|
||||||
*p ^= xorWordMask
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; i < n; i++ {
|
|
||||||
b[i] ^= XORKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//go:build !amd64 && !arm64 && !arm && !386
|
|
||||||
|
|
||||||
package protocol
|
|
||||||
|
|
||||||
// Generic fallback for 32-bit and uncommon architectures.
|
|
||||||
func XorInPlace(b []byte) {
|
|
||||||
for i := range b {
|
|
||||||
b[i] ^= XORKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
package wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
RequestHeaderSize = 29
|
|
||||||
ResponseHeaderSize = 5
|
|
||||||
MaxPayload = 2 * 1024 * 1024
|
|
||||||
|
|
||||||
ModeProbe byte = 0
|
|
||||||
ModeOpen byte = 1
|
|
||||||
ModeUpload byte = 2
|
|
||||||
ModeDownload byte = 3
|
|
||||||
ModeClose byte = 4
|
|
||||||
|
|
||||||
StatusOK byte = 0
|
|
||||||
StatusError byte = 1
|
|
||||||
StatusData byte = 2
|
|
||||||
StatusWait byte = 3
|
|
||||||
StatusEOF byte = 4
|
|
||||||
|
|
||||||
ProbeUpload byte = 1
|
|
||||||
ProbeDownload byte = 2
|
|
||||||
ProbeKeepalive byte = 3
|
|
||||||
ProbeBatch byte = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
var ProbeMagic = [4]byte{'D', 'T', 'P', '2'}
|
|
||||||
|
|
||||||
type SessionID [16]byte
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Mode byte
|
|
||||||
Session SessionID
|
|
||||||
Seq uint64
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func MaskInPlace(data []byte, sid SessionID, mode byte, seq uint64, response bool) {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var seed [30]byte
|
|
||||||
copy(seed[:16], sid[:])
|
|
||||||
seed[16] = mode
|
|
||||||
binary.BigEndian.PutUint64(seed[17:25], seq)
|
|
||||||
if response {
|
|
||||||
seed[25] = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var counter uint32
|
|
||||||
for off := 0; off < len(data); {
|
|
||||||
binary.BigEndian.PutUint32(seed[26:30], counter)
|
|
||||||
block := sha256.Sum256(seed[:])
|
|
||||||
n := len(data) - off
|
|
||||||
if n > len(block) {
|
|
||||||
n = len(block)
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
data[off+i] ^= block[i]
|
|
||||||
}
|
|
||||||
off += n
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte) error {
|
|
||||||
if len(plaintext) > MaxPayload {
|
|
||||||
return fmt.Errorf("request payload too large: %d", len(plaintext))
|
|
||||||
}
|
|
||||||
|
|
||||||
packet := make([]byte, RequestHeaderSize+len(plaintext))
|
|
||||||
packet[0] = mode
|
|
||||||
copy(packet[1:17], sid[:])
|
|
||||||
binary.BigEndian.PutUint64(packet[17:25], seq)
|
|
||||||
binary.BigEndian.PutUint32(packet[25:29], uint32(len(plaintext)))
|
|
||||||
copy(packet[29:], plaintext)
|
|
||||||
MaskInPlace(packet[29:], sid, mode, seq, false)
|
|
||||||
return writeAll(w, packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadRequest(r io.Reader) (Request, error) {
|
|
||||||
var req Request
|
|
||||||
var header [RequestHeaderSize]byte
|
|
||||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
|
||||||
return req, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Mode = header[0]
|
|
||||||
copy(req.Session[:], header[1:17])
|
|
||||||
req.Seq = binary.BigEndian.Uint64(header[17:25])
|
|
||||||
n := binary.BigEndian.Uint32(header[25:29])
|
|
||||||
if n > MaxPayload {
|
|
||||||
return req, errors.New("request payload too large")
|
|
||||||
}
|
|
||||||
|
|
||||||
if n > 0 {
|
|
||||||
req.Payload = make([]byte, int(n))
|
|
||||||
if _, err := io.ReadFull(r, req.Payload); err != nil {
|
|
||||||
return req, err
|
|
||||||
}
|
|
||||||
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
|
|
||||||
}
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteResponse(w io.Writer, status byte, body []byte) error {
|
|
||||||
if len(body) > MaxPayload {
|
|
||||||
return fmt.Errorf("response body too large: %d", len(body))
|
|
||||||
}
|
|
||||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
|
||||||
packet[0] = status
|
|
||||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
|
||||||
copy(packet[5:], body)
|
|
||||||
return writeAll(w, packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64) error {
|
|
||||||
if len(body) > MaxPayload {
|
|
||||||
return fmt.Errorf("response body too large: %d", len(body))
|
|
||||||
}
|
|
||||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
|
||||||
packet[0] = status
|
|
||||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
|
||||||
copy(packet[5:], body)
|
|
||||||
MaskInPlace(packet[5:], sid, mode, seq, true)
|
|
||||||
return writeAll(w, packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadResponse(r io.Reader) (byte, []byte, error) {
|
|
||||||
var header [ResponseHeaderSize]byte
|
|
||||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
n := binary.BigEndian.Uint32(header[1:5])
|
|
||||||
if n > MaxPayload {
|
|
||||||
return 0, nil, errors.New("response body too large")
|
|
||||||
}
|
|
||||||
var body []byte
|
|
||||||
if n > 0 {
|
|
||||||
body = make([]byte, int(n))
|
|
||||||
if _, err := io.ReadFull(r, body); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return header[0], body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeMaskedResponse(status byte, body []byte, sid SessionID, mode byte, seq uint64) []byte {
|
|
||||||
if len(body) == 0 || status == StatusError {
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
out := append([]byte(nil), body...)
|
|
||||||
MaskInPlace(out, sid, mode, seq, true)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAll(w io.Writer, b []byte) error {
|
|
||||||
for len(b) > 0 {
|
|
||||||
n, err := w.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n <= 0 {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
b = b[n:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMaskChangesWithSequenceAndRoundTrips(t *testing.T) {
|
|
||||||
var sid SessionID
|
|
||||||
for i := range sid { sid[i] = byte(i+1) }
|
|
||||||
plain := bytes.Repeat([]byte("DragonTCP"), 100)
|
|
||||||
a := append([]byte(nil), plain...)
|
|
||||||
b := append([]byte(nil), plain...)
|
|
||||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
|
||||||
MaskInPlace(b, sid, ModeUpload, 2, false)
|
|
||||||
if bytes.Equal(a, b) { t.Fatal("different sequences produced identical wire bytes") }
|
|
||||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
|
||||||
if !bytes.Equal(a, plain) { t.Fatal("mask did not round-trip") }
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkMask1MiB(b *testing.B) {
|
|
||||||
var sid SessionID
|
|
||||||
data := make([]byte, 1024*1024)
|
|
||||||
b.SetBytes(int64(len(data)))
|
|
||||||
b.ResetTimer()
|
|
||||||
for i:=0;i<b.N;i++ {
|
|
||||||
MaskInPlace(data,sid,ModeUpload,uint64(i),false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
+26
-1
@@ -40,9 +40,15 @@ public class DragonService extends VpnService {
|
|||||||
public static final String EXTRA_TOKEN = "token";
|
public static final String EXTRA_TOKEN = "token";
|
||||||
public static final String EXTRA_CHUNK_MAX = "chunkMax";
|
public static final String EXTRA_CHUNK_MAX = "chunkMax";
|
||||||
public static final String EXTRA_CHUNK_MIN = "chunkMin";
|
public static final String EXTRA_CHUNK_MIN = "chunkMin";
|
||||||
|
/** Maximum download records per request. A transport setting, not a thread count. */
|
||||||
|
public static final String EXTRA_BATCH_MAX = "batchMax";
|
||||||
|
/** Minimum download records per request; equal to the maximum pins the depth. */
|
||||||
|
public static final String EXTRA_BATCH_MIN = "batchMin";
|
||||||
public static final String EXTRA_RECONNECT = "reconnect";
|
public static final String EXTRA_RECONNECT = "reconnect";
|
||||||
public static final String EXTRA_TIMEOUT = "timeout";
|
public static final String EXTRA_TIMEOUT = "timeout";
|
||||||
|
|
||||||
|
private static final int BATCH_LIMIT = 256;
|
||||||
|
|
||||||
private static final int NOTIFICATION_ID = 53;
|
private static final int NOTIFICATION_ID = 53;
|
||||||
private static final String CHANNEL_ID = "dragontcp-lite";
|
private static final String CHANNEL_ID = "dragontcp-lite";
|
||||||
private static final int LOCAL_PROXY_PORT = 8080;
|
private static final int LOCAL_PROXY_PORT = 8080;
|
||||||
@@ -106,6 +112,8 @@ public class DragonService extends VpnService {
|
|||||||
String token = intent.getStringExtra(EXTRA_TOKEN);
|
String token = intent.getStringExtra(EXTRA_TOKEN);
|
||||||
int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
|
int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
|
||||||
int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
|
int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
|
||||||
|
int batchMax = intent.getIntExtra(EXTRA_BATCH_MAX, 1);
|
||||||
|
int batchMin = intent.getIntExtra(EXTRA_BATCH_MIN, 1);
|
||||||
int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 0);
|
int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 0);
|
||||||
int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 2);
|
int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 2);
|
||||||
|
|
||||||
@@ -117,12 +125,15 @@ public class DragonService extends VpnService {
|
|||||||
if (token == null) token = "";
|
if (token == null) token = "";
|
||||||
chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
|
chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
|
||||||
chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
|
chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
|
||||||
|
batchMax = Math.max(1, Math.min(BATCH_LIMIT, batchMax));
|
||||||
|
batchMin = Math.max(1, Math.min(batchMax, batchMin));
|
||||||
reconnect = Math.max(0, reconnect);
|
reconnect = Math.max(0, reconnect);
|
||||||
timeout = Math.max(1, timeout);
|
timeout = Math.max(1, timeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
AppLog.append("Starting DragonTCP → " + server + ":" + port);
|
AppLog.append("Starting DragonTCP → " + server + ":" + port);
|
||||||
Process process = startDragonCore(server, port, token, chunkMax, chunkMin, reconnect, timeout);
|
AppLog.append(describeBatch(batchMin, batchMax));
|
||||||
|
Process process = startDragonCore(server, port, token, chunkMax, chunkMin, batchMax, batchMin, reconnect, timeout);
|
||||||
synchronized (stateLock) { coreProcess = process; }
|
synchronized (stateLock) { coreProcess = process; }
|
||||||
|
|
||||||
startCoreLogReader(process);
|
startCoreLogReader(process);
|
||||||
@@ -186,6 +197,8 @@ public class DragonService extends VpnService {
|
|||||||
String token,
|
String token,
|
||||||
int chunkMax,
|
int chunkMax,
|
||||||
int chunkMin,
|
int chunkMin,
|
||||||
|
int batchMax,
|
||||||
|
int batchMin,
|
||||||
int reconnect,
|
int reconnect,
|
||||||
int timeout
|
int timeout
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
@@ -204,6 +217,8 @@ public class DragonService extends VpnService {
|
|||||||
cmd.add("--chunk-min"); cmd.add(Integer.toString(chunkMin));
|
cmd.add("--chunk-min"); cmd.add(Integer.toString(chunkMin));
|
||||||
cmd.add("--chunk-max"); cmd.add(Integer.toString(chunkMax));
|
cmd.add("--chunk-max"); cmd.add(Integer.toString(chunkMax));
|
||||||
cmd.add("--chunk-pollers"); cmd.add("1");
|
cmd.add("--chunk-pollers"); cmd.add("1");
|
||||||
|
cmd.add("--chunk-concurrency"); cmd.add(Integer.toString(batchMax));
|
||||||
|
cmd.add("--chunk-concurrency-min"); cmd.add(Integer.toString(batchMin));
|
||||||
cmd.add("--chunk-reconnect-every"); cmd.add(Integer.toString(reconnect));
|
cmd.add("--chunk-reconnect-every"); cmd.add(Integer.toString(reconnect));
|
||||||
cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
|
cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
|
||||||
cmd.add("--chunk-grow-after"); cmd.add("16");
|
cmd.add("--chunk-grow-after"); cmd.add("16");
|
||||||
@@ -214,6 +229,16 @@ public class DragonService extends VpnService {
|
|||||||
return pb.start();
|
return pb.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Human-readable summary of the download batch configuration, for the log screen. */
|
||||||
|
private static String describeBatch(int min, int max) {
|
||||||
|
if (min == max) {
|
||||||
|
return max == 1
|
||||||
|
? "Download batch: fixed at 1 record per request"
|
||||||
|
: "Download batch: pinned at " + max + " records per request (never adapts)";
|
||||||
|
}
|
||||||
|
return "Download batch: adaptive " + min + "-" + max + " records per request";
|
||||||
|
}
|
||||||
|
|
||||||
private void startCoreLogReader(Process process) {
|
private void startCoreLogReader(Process process) {
|
||||||
Thread reader = new Thread(() -> {
|
Thread reader = new Thread(() -> {
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
+109
-8
@@ -12,7 +12,9 @@ import android.graphics.drawable.GradientDrawable;
|
|||||||
import android.net.VpnService;
|
import android.net.VpnService;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.text.TextWatcher;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -28,6 +30,8 @@ public class MainActivity extends Activity {
|
|||||||
private static final int VPN_REQUEST = 100;
|
private static final int VPN_REQUEST = 100;
|
||||||
private static final String PREFS = "dragontcp";
|
private static final String PREFS = "dragontcp";
|
||||||
|
|
||||||
|
private static final int BATCH_LIMIT = 256;
|
||||||
|
|
||||||
private static final int BG = Color.rgb(11, 15, 20);
|
private static final int BG = Color.rgb(11, 15, 20);
|
||||||
private static final int CARD = Color.rgb(22, 28, 36);
|
private static final int CARD = Color.rgb(22, 28, 36);
|
||||||
private static final int FIELD = Color.rgb(14, 19, 26);
|
private static final int FIELD = Color.rgb(14, 19, 26);
|
||||||
@@ -39,15 +43,19 @@ public class MainActivity extends Activity {
|
|||||||
private static final int DISABLED = Color.rgb(48, 56, 68);
|
private static final int DISABLED = Color.rgb(48, 56, 68);
|
||||||
private static final int OK = Color.rgb(82, 201, 143);
|
private static final int OK = Color.rgb(82, 201, 143);
|
||||||
private static final int WARN = Color.rgb(240, 177, 83);
|
private static final int WARN = Color.rgb(240, 177, 83);
|
||||||
|
private static final int BAD = Color.rgb(232, 116, 116);
|
||||||
|
|
||||||
private EditText server;
|
private EditText server;
|
||||||
private EditText port;
|
private EditText port;
|
||||||
private EditText token;
|
private EditText token;
|
||||||
private EditText chunkMax;
|
private EditText chunkMax;
|
||||||
private EditText chunkMin;
|
private EditText chunkMin;
|
||||||
|
private EditText batchMax;
|
||||||
|
private EditText batchMin;
|
||||||
private EditText reconnect;
|
private EditText reconnect;
|
||||||
private EditText timeout;
|
private EditText timeout;
|
||||||
|
|
||||||
|
private TextView batchHint;
|
||||||
private Button connectButton;
|
private Button connectButton;
|
||||||
private Button stopButton;
|
private Button stopButton;
|
||||||
private Button logsButton;
|
private Button logsButton;
|
||||||
@@ -73,6 +81,7 @@ public class MainActivity extends Activity {
|
|||||||
forceDarkSystemUi();
|
forceDarkSystemUi();
|
||||||
buildUi();
|
buildUi();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
updateBatchHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -141,6 +150,7 @@ public class MainActivity extends Activity {
|
|||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- connection
|
||||||
LinearLayout connectionCard = card("CONNECTION");
|
LinearLayout connectionCard = card("CONNECTION");
|
||||||
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
|
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
|
||||||
LinearLayout connectionRow = row();
|
LinearLayout connectionRow = row();
|
||||||
@@ -149,22 +159,54 @@ public class MainActivity extends Activity {
|
|||||||
connectionCard.addView(connectionRow);
|
connectionCard.addView(connectionRow);
|
||||||
settings.addView(connectionCard, cardParams());
|
settings.addView(connectionCard, cardParams());
|
||||||
|
|
||||||
LinearLayout transportCard = card("TRANSPORT");
|
// --------------------------------------------------------- record size
|
||||||
|
LinearLayout sizeCard = card("RECORD SIZE (BYTES)");
|
||||||
LinearLayout chunks = row();
|
LinearLayout chunks = row();
|
||||||
chunkMax = addFieldToRow(chunks, "Max chunk", "1048576", "1048576", true, false, 0.58f);
|
chunkMax = addFieldToRow(chunks, "Max chunk", "1048576", "1048576", true, false, 0.58f);
|
||||||
chunkMin = addFieldToRow(chunks, "Min chunk", "32", "32", true, false, 0.42f);
|
chunkMin = addFieldToRow(chunks, "Min chunk", "32", "32", true, false, 0.42f);
|
||||||
transportCard.addView(chunks);
|
sizeCard.addView(chunks);
|
||||||
|
sizeCard.addView(hint("Probed automatically on connect, then adapted if the path changes."));
|
||||||
|
settings.addView(sizeCard, cardParams());
|
||||||
|
|
||||||
|
// ------------------------------------------------------- download batch
|
||||||
|
LinearLayout batchCard = card("DOWNLOAD BATCH");
|
||||||
|
batchCard.addView(hint(
|
||||||
|
"How many records one download request may return. This is a transport "
|
||||||
|
+ "setting, not a thread count."
|
||||||
|
));
|
||||||
|
LinearLayout batch = row();
|
||||||
|
batchMax = addFieldToRow(batch, "Batch max", "1", "1", true, false, 0.5f);
|
||||||
|
batchMin = addFieldToRow(batch, "Batch min", "1", "1", true, false, 0.5f);
|
||||||
|
batchCard.addView(batch);
|
||||||
|
|
||||||
|
batchHint = text("", 11, MUTED, true);
|
||||||
|
batchHint.setLineSpacing(0, 1.08f);
|
||||||
|
batchHint.setPadding(dp(2), dp(2), dp(2), dp(2));
|
||||||
|
batchCard.addView(batchHint);
|
||||||
|
batchCard.addView(hint(
|
||||||
|
"Set both to the same number to pin the batch: the depth never grows or "
|
||||||
|
+ "shrinks, which is what paths that only work at one specific size need."
|
||||||
|
));
|
||||||
|
settings.addView(batchCard, cardParams());
|
||||||
|
|
||||||
|
TextWatcher batchWatcher = new TextWatcher() {
|
||||||
|
@Override public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
|
||||||
|
@Override public void onTextChanged(CharSequence s, int a, int b, int c) {}
|
||||||
|
@Override public void afterTextChanged(Editable s) { updateBatchHint(); }
|
||||||
|
};
|
||||||
|
batchMax.addTextChangedListener(batchWatcher);
|
||||||
|
batchMin.addTextChangedListener(batchWatcher);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ advanced
|
||||||
|
LinearLayout advancedCard = card("ADVANCED");
|
||||||
LinearLayout timing = row();
|
LinearLayout timing = row();
|
||||||
reconnect = addFieldToRow(timing, "Reconnect every", "0 = persistent", "0", true, false, 0.58f);
|
reconnect = addFieldToRow(timing, "Reconnect every", "0 = persistent", "0", true, false, 0.58f);
|
||||||
timeout = addFieldToRow(timing, "Timeout (s)", "2", "2", true, false, 0.42f);
|
timeout = addFieldToRow(timing, "Timeout (s)", "2", "2", true, false, 0.42f);
|
||||||
transportCard.addView(timing);
|
advancedCard.addView(timing);
|
||||||
|
advancedCard.addView(hint("0 reconnect = persistent • 1 = auto • N = rotate every N requests"));
|
||||||
TextView fixed = text("Auto path probe • 0 reconnect = persistent", 11, MUTED, false);
|
settings.addView(advancedCard, cardParams());
|
||||||
fixed.setPadding(dp(2), dp(6), dp(2), dp(2));
|
|
||||||
transportCard.addView(fixed);
|
|
||||||
settings.addView(transportCard, cardParams());
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- buttons
|
||||||
LinearLayout buttons = row();
|
LinearLayout buttons = row();
|
||||||
buttons.setPadding(0, dp(4), 0, dp(8));
|
buttons.setPadding(0, dp(4), 0, dp(8));
|
||||||
connectButton = actionButton("CONNECT");
|
connectButton = actionButton("CONNECT");
|
||||||
@@ -207,6 +249,49 @@ public class MainActivity extends Activity {
|
|||||||
updateConnectionUi(false, "DISCONNECTED");
|
updateConnectionUi(false, "DISCONNECTED");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Describes the batch configuration in words, live, as the user types. */
|
||||||
|
private void updateBatchHint() {
|
||||||
|
if (batchHint == null) return;
|
||||||
|
Integer max = readInt(batchMax);
|
||||||
|
Integer min = readInt(batchMin);
|
||||||
|
|
||||||
|
if (max == null || min == null) {
|
||||||
|
batchHint.setTextColor(MUTED);
|
||||||
|
batchHint.setText("Enter a value from 1 to " + BATCH_LIMIT + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (max < 1 || max > BATCH_LIMIT || min < 1 || min > BATCH_LIMIT) {
|
||||||
|
batchHint.setTextColor(BAD);
|
||||||
|
batchHint.setText("Batch values must be 1-" + BATCH_LIMIT + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (min > max) {
|
||||||
|
batchHint.setTextColor(BAD);
|
||||||
|
batchHint.setText("Batch min must not be greater than batch max.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (min == max) {
|
||||||
|
batchHint.setTextColor(OK);
|
||||||
|
if (max == 1) {
|
||||||
|
batchHint.setText("Fixed: 1 record per request, never adapts.");
|
||||||
|
} else {
|
||||||
|
batchHint.setText("Pinned: exactly " + max + " records per request, never adapts.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchHint.setTextColor(ACCENT);
|
||||||
|
batchHint.setText("Adaptive: starts at " + max + ", falls back toward " + min
|
||||||
|
+ " on errors, recovers to " + max + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer readInt(EditText field) {
|
||||||
|
if (field == null) return null;
|
||||||
|
String raw = field.getText().toString().trim();
|
||||||
|
if (raw.isEmpty()) return null;
|
||||||
|
try { return Integer.valueOf(Integer.parseInt(raw)); }
|
||||||
|
catch (Exception e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
private LinearLayout card(String title) {
|
private LinearLayout card(String title) {
|
||||||
LinearLayout card = new LinearLayout(this);
|
LinearLayout card = new LinearLayout(this);
|
||||||
card.setOrientation(LinearLayout.VERTICAL);
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
@@ -219,6 +304,13 @@ public class MainActivity extends Activity {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TextView hint(String value) {
|
||||||
|
TextView t = text(value, 11, MUTED, false);
|
||||||
|
t.setLineSpacing(0, 1.08f);
|
||||||
|
t.setPadding(dp(2), dp(2), dp(2), dp(6));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
private LinearLayout.LayoutParams cardParams() {
|
private LinearLayout.LayoutParams cardParams() {
|
||||||
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -356,6 +448,8 @@ public class MainActivity extends Activity {
|
|||||||
i.putExtra(DragonService.EXTRA_TOKEN, p.getString("token", ""));
|
i.putExtra(DragonService.EXTRA_TOKEN, p.getString("token", ""));
|
||||||
i.putExtra(DragonService.EXTRA_CHUNK_MAX, p.getInt("max", 1048576));
|
i.putExtra(DragonService.EXTRA_CHUNK_MAX, p.getInt("max", 1048576));
|
||||||
i.putExtra(DragonService.EXTRA_CHUNK_MIN, p.getInt("min", 32));
|
i.putExtra(DragonService.EXTRA_CHUNK_MIN, p.getInt("min", 32));
|
||||||
|
i.putExtra(DragonService.EXTRA_BATCH_MAX, p.getInt("batchMax", 1));
|
||||||
|
i.putExtra(DragonService.EXTRA_BATCH_MIN, p.getInt("batchMin", 1));
|
||||||
i.putExtra(DragonService.EXTRA_RECONNECT, p.getInt("reconnect", 0));
|
i.putExtra(DragonService.EXTRA_RECONNECT, p.getInt("reconnect", 0));
|
||||||
i.putExtra(DragonService.EXTRA_TIMEOUT, p.getInt("timeout", 2));
|
i.putExtra(DragonService.EXTRA_TIMEOUT, p.getInt("timeout", 2));
|
||||||
if (Build.VERSION.SDK_INT >= 26) startForegroundService(i); else startService(i);
|
if (Build.VERSION.SDK_INT >= 26) startForegroundService(i); else startService(i);
|
||||||
@@ -367,6 +461,9 @@ public class MainActivity extends Activity {
|
|||||||
int p = parse(port, 1, 65535, "Port");
|
int p = parse(port, 1, 65535, "Port");
|
||||||
int max = parse(chunkMax, 32, 1048576, "Max chunk");
|
int max = parse(chunkMax, 32, 1048576, "Max chunk");
|
||||||
int min = parse(chunkMin, 32, max, "Min chunk");
|
int min = parse(chunkMin, 32, max, "Min chunk");
|
||||||
|
int bMax = parse(batchMax, 1, BATCH_LIMIT, "Batch max");
|
||||||
|
int bMin = parse(batchMin, 1, BATCH_LIMIT, "Batch min");
|
||||||
|
if (bMin > bMax) throw new IllegalArgumentException("Batch min must not exceed batch max");
|
||||||
int rec = parse(reconnect, 0, 1000000, "Reconnect every");
|
int rec = parse(reconnect, 0, 1000000, "Reconnect every");
|
||||||
int tout = parse(timeout, 1, 120, "Timeout");
|
int tout = parse(timeout, 1, 120, "Timeout");
|
||||||
|
|
||||||
@@ -376,6 +473,8 @@ public class MainActivity extends Activity {
|
|||||||
.putString("token", token.getText().toString())
|
.putString("token", token.getText().toString())
|
||||||
.putInt("max", max)
|
.putInt("max", max)
|
||||||
.putInt("min", min)
|
.putInt("min", min)
|
||||||
|
.putInt("batchMax", bMax)
|
||||||
|
.putInt("batchMin", bMin)
|
||||||
.putInt("reconnect", rec)
|
.putInt("reconnect", rec)
|
||||||
.putInt("timeout", tout)
|
.putInt("timeout", tout)
|
||||||
.apply();
|
.apply();
|
||||||
@@ -396,6 +495,8 @@ public class MainActivity extends Activity {
|
|||||||
token.setText(p.getString("token", ""));
|
token.setText(p.getString("token", ""));
|
||||||
chunkMax.setText(Integer.toString(p.getInt("max", 1048576)));
|
chunkMax.setText(Integer.toString(p.getInt("max", 1048576)));
|
||||||
chunkMin.setText(Integer.toString(p.getInt("min", 32)));
|
chunkMin.setText(Integer.toString(p.getInt("min", 32)));
|
||||||
|
batchMax.setText(Integer.toString(p.getInt("batchMax", 1)));
|
||||||
|
batchMin.setText(Integer.toString(p.getInt("batchMin", 1)));
|
||||||
reconnect.setText(Integer.toString(p.getInt("reconnect", 0)));
|
reconnect.setText(Integer.toString(p.getInt("reconnect", 0)));
|
||||||
timeout.setText(Integer.toString(p.getInt("timeout", 2)));
|
timeout.setText(Integer.toString(p.getInt("timeout", 2)));
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
-2
@@ -15,12 +15,17 @@
|
|||||||
param(
|
param(
|
||||||
# Only build the Android client .so (skip the Linux servers).
|
# Only build the Android client .so (skip the Linux servers).
|
||||||
[switch]$ClientOnly,
|
[switch]$ClientOnly,
|
||||||
|
# Where to drop libdragontcp_client.so. Defaults to android\lib\arm64-v8a
|
||||||
|
# under this script; build_apk.ps1 passes its own app directory explicitly,
|
||||||
|
# which is what makes this work regardless of how the tree is nested.
|
||||||
|
[string]$AndroidLibDir,
|
||||||
# Path to the go executable.
|
# Path to the go executable.
|
||||||
[string]$GoBin = $(if ($env:GO_BIN) { $env:GO_BIN } else { 'go' })
|
[string]$GoBin = $(if ($env:GO_BIN) { $env:GO_BIN } else { 'go' })
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$Root = $PSScriptRoot
|
$Root = $PSScriptRoot
|
||||||
|
if (-not $AndroidLibDir) { $AndroidLibDir = Join-Path $Root 'android\lib\arm64-v8a' }
|
||||||
|
|
||||||
function Invoke-Go {
|
function Invoke-Go {
|
||||||
param([hashtable]$Env, [string[]]$GoArgs)
|
param([hashtable]$Env, [string[]]$GoArgs)
|
||||||
@@ -40,16 +45,20 @@ function Invoke-Go {
|
|||||||
$go = Get-Command $GoBin -ErrorAction SilentlyContinue
|
$go = Get-Command $GoBin -ErrorAction SilentlyContinue
|
||||||
if (-not $go) { throw "Go compiler not found. Install from https://go.dev/dl/ or set -GoBin." }
|
if (-not $go) { throw "Go compiler not found. Install from https://go.dev/dl/ or set -GoBin." }
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path "$Root\bin", "$Root\android\lib\arm64-v8a" | Out-Null
|
New-Item -ItemType Directory -Force -Path "$Root\bin", $AndroidLibDir | Out-Null
|
||||||
Push-Location "$Root\core"
|
Push-Location "$Root\core"
|
||||||
try {
|
try {
|
||||||
$ldflags = '-ldflags=-s -w'
|
$ldflags = '-ldflags=-s -w'
|
||||||
|
|
||||||
Write-Host '[core] Android ARM64 client...' -ForegroundColor Cyan
|
Write-Host '[core] Android ARM64 client...' -ForegroundColor Cyan
|
||||||
Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'android'; GOARCH = 'arm64' } `
|
Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'android'; GOARCH = 'arm64' } `
|
||||||
@('build', '-trimpath', $ldflags, '-o', "$Root\android\lib\arm64-v8a\libdragontcp_client.so", './cmd/dragontcp-client')
|
@('build', '-trimpath', $ldflags, '-o', (Join-Path $AndroidLibDir 'libdragontcp_client.so'), './cmd/dragontcp-client')
|
||||||
|
|
||||||
if (-not $ClientOnly) {
|
if (-not $ClientOnly) {
|
||||||
|
Write-Host '[core] Linux AMD64 client...' -ForegroundColor Cyan
|
||||||
|
Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'linux'; GOARCH = 'amd64' } `
|
||||||
|
@('build', '-trimpath', $ldflags, '-o', "$Root\bin\dragontcp-hybrid-client-linux-amd64", './cmd/dragontcp-client')
|
||||||
|
|
||||||
Write-Host '[core] Linux AMD64 server...' -ForegroundColor Cyan
|
Write-Host '[core] Linux AMD64 server...' -ForegroundColor Cyan
|
||||||
Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'linux'; GOARCH = 'amd64' } `
|
Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'linux'; GOARCH = 'amd64' } `
|
||||||
@('build', '-trimpath', $ldflags, '-o', "$Root\bin\dragontcp-hybrid-server-linux-amd64", './cmd/dragontcp-server')
|
@('build', '-trimpath', $ldflags, '-o', "$Root\bin\dragontcp-hybrid-server-linux-amd64", './cmd/dragontcp-server')
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type chunkClientOptions struct {
|
|||||||
pollDelay time.Duration
|
pollDelay time.Duration
|
||||||
txnTimeout time.Duration
|
txnTimeout time.Duration
|
||||||
tcpBuffer int
|
tcpBuffer int
|
||||||
|
minPipeline int
|
||||||
maxPipeline int
|
maxPipeline int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +527,7 @@ type chunkConn struct {
|
|||||||
consumedOffset uint64
|
consumedOffset uint64
|
||||||
eof bool
|
eof bool
|
||||||
pipeline int
|
pipeline int
|
||||||
|
minPipeline int
|
||||||
maxPipeline int
|
maxPipeline int
|
||||||
|
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
@@ -556,6 +558,12 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
|||||||
if opts.maxPipeline > 256 {
|
if opts.maxPipeline > 256 {
|
||||||
opts.maxPipeline = 256
|
opts.maxPipeline = 256
|
||||||
}
|
}
|
||||||
|
if opts.minPipeline < 1 {
|
||||||
|
opts.minPipeline = 1
|
||||||
|
}
|
||||||
|
if opts.minPipeline > opts.maxPipeline {
|
||||||
|
opts.minPipeline = opts.maxPipeline
|
||||||
|
}
|
||||||
|
|
||||||
profile := getPathProfile(serverAddr, token, opts)
|
profile := getPathProfile(serverAddr, token, opts)
|
||||||
reconnect := opts.reconnectEvery
|
reconnect := opts.reconnectEvery
|
||||||
@@ -620,8 +628,11 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
|||||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||||
// Start at the user-configured ceiling. On transport failures the
|
// Start at the user-configured ceiling. On transport failures the
|
||||||
// pipeline is halved; successful data responses grow it back by one,
|
// pipeline is halved; successful data responses grow it back by one,
|
||||||
// always staying inside 1..maxPipeline. A ceiling of 1 is fixed.
|
// always staying inside minPipeline..maxPipeline. When the two bounds
|
||||||
|
// are equal the depth is pinned and never adapts, which is what paths
|
||||||
|
// that only work at one specific batch size need.
|
||||||
pipeline: opts.maxPipeline,
|
pipeline: opts.maxPipeline,
|
||||||
|
minPipeline: opts.minPipeline,
|
||||||
maxPipeline: opts.maxPipeline,
|
maxPipeline: opts.maxPipeline,
|
||||||
}
|
}
|
||||||
c.upSizer = newAdaptiveSizer("upload", upStart, opts)
|
c.upSizer = newAdaptiveSizer("upload", upStart, opts)
|
||||||
@@ -629,6 +640,53 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pinnedBatch reports whether the batch depth is fixed. A pinned depth never
|
||||||
|
// grows or shrinks: some paths only deliver correctly at one specific number of
|
||||||
|
// records per request, so the adaptive controller must stay out of the way.
|
||||||
|
func (c *chunkConn) pinnedBatch() bool { return c.minPipeline >= c.maxPipeline }
|
||||||
|
|
||||||
|
// batchCount is how many records the next download request will ask for.
|
||||||
|
func (c *chunkConn) batchCount(chunk int) int {
|
||||||
|
count := c.pipeline
|
||||||
|
if count < c.minPipeline {
|
||||||
|
count = c.minPipeline
|
||||||
|
}
|
||||||
|
if count > c.maxPipeline {
|
||||||
|
count = c.maxPipeline
|
||||||
|
}
|
||||||
|
// Bound each batch to roughly 1 MiB of useful data, but never below the
|
||||||
|
// configured floor: a pinned depth is a path requirement, not a hint.
|
||||||
|
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
||||||
|
count = maxInt(maxCount, c.minPipeline)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// growPipeline widens the batch by one after a successful data response.
|
||||||
|
func (c *chunkConn) growPipeline() {
|
||||||
|
if c.pinnedBatch() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.pipeline < c.maxPipeline {
|
||||||
|
c.pipeline++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shrinkPipeline halves the batch after a transport failure. It reports the old
|
||||||
|
// and new depth, and whether anything actually changed; when it returns false
|
||||||
|
// the caller should shrink the record size instead.
|
||||||
|
func (c *chunkConn) shrinkPipeline() (int, int, bool) {
|
||||||
|
if c.pinnedBatch() || c.pipeline <= c.minPipeline {
|
||||||
|
return c.pipeline, c.pipeline, false
|
||||||
|
}
|
||||||
|
old := c.pipeline
|
||||||
|
c.pipeline /= 2
|
||||||
|
if c.pipeline < c.minPipeline {
|
||||||
|
c.pipeline = c.minPipeline
|
||||||
|
}
|
||||||
|
return old, c.pipeline, old != c.pipeline
|
||||||
|
}
|
||||||
|
|
||||||
func (c *chunkConn) fillReadBuffer() error {
|
func (c *chunkConn) fillReadBuffer() error {
|
||||||
if c.eof {
|
if c.eof {
|
||||||
return io.EOF
|
return io.EOF
|
||||||
@@ -636,17 +694,7 @@ func (c *chunkConn) fillReadBuffer() error {
|
|||||||
minFailures := 0
|
minFailures := 0
|
||||||
for len(c.readBuf) == 0 && !c.eof {
|
for len(c.readBuf) == 0 && !c.eof {
|
||||||
chunk := c.downSizer.Current()
|
chunk := c.downSizer.Current()
|
||||||
count := c.pipeline
|
count := c.batchCount(chunk)
|
||||||
if count < 1 {
|
|
||||||
count = 1
|
|
||||||
}
|
|
||||||
if count > c.maxPipeline {
|
|
||||||
count = c.maxPipeline
|
|
||||||
}
|
|
||||||
// Bound each batch to roughly 1 MiB of useful data.
|
|
||||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
|
||||||
count = maxInt(maxCount, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
||||||
for _, part := range data {
|
for _, part := range data {
|
||||||
@@ -655,20 +703,16 @@ func (c *chunkConn) fillReadBuffer() error {
|
|||||||
}
|
}
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
c.downSizer.Success(chunk)
|
c.downSizer.Success(chunk)
|
||||||
if c.pipeline < c.maxPipeline {
|
c.growPipeline()
|
||||||
c.pipeline++
|
|
||||||
}
|
|
||||||
minFailures = 0
|
minFailures = 0
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.pipeline > 1 {
|
// Shrink the batch first, then the record size. When the batch is
|
||||||
old := c.pipeline
|
// pinned (min == max) the depth is left alone entirely and only the
|
||||||
c.pipeline /= 2
|
// record size adapts.
|
||||||
if c.pipeline < 1 {
|
if old, next, shrank := c.shrinkPipeline(); shrank {
|
||||||
c.pipeline = 1
|
if c.opts.adaptLog {
|
||||||
}
|
fmt.Printf("adaptive download batch: %d -> %d after transport failure\n", old, next)
|
||||||
if c.opts.adaptLog && old != c.pipeline {
|
|
||||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure\n", old, c.pipeline)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
old, next := c.downSizer.Failure(chunk)
|
old, next := c.downSizer.Failure(chunk)
|
||||||
|
|||||||
@@ -23,6 +23,76 @@ func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTestConn(min, max int) *chunkConn {
|
||||||
|
return &chunkConn{pipeline: max, minPipeline: min, maxPipeline: max}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPinnedBatchNeverAdapts(t *testing.T) {
|
||||||
|
c := newTestConn(5, 5)
|
||||||
|
if got := c.batchCount(1400); got != 5 {
|
||||||
|
t.Fatalf("pinned batch should request 5 records, got %d", got)
|
||||||
|
}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if _, _, shrank := c.shrinkPipeline(); shrank {
|
||||||
|
t.Fatal("pinned batch shrank on transport failure")
|
||||||
|
}
|
||||||
|
c.growPipeline()
|
||||||
|
}
|
||||||
|
if c.pipeline != 5 {
|
||||||
|
t.Fatalf("pinned batch drifted to %d", c.pipeline)
|
||||||
|
}
|
||||||
|
if got := c.batchCount(1400); got != 5 {
|
||||||
|
t.Fatalf("pinned batch should still request 5 records, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPinnedBatchSurvivesOneMiBCap(t *testing.T) {
|
||||||
|
// 8 x 1 MiB records exceed the ~1 MiB useful-data cap. A pinned depth must
|
||||||
|
// win anyway, otherwise a path that needs exactly 8 records is broken by
|
||||||
|
// an unrelated size heuristic.
|
||||||
|
c := newTestConn(8, 8)
|
||||||
|
if got := c.batchCount(1024 * 1024); got != 8 {
|
||||||
|
t.Fatalf("pinned batch should ignore the 1 MiB cap, got %d", got)
|
||||||
|
}
|
||||||
|
// An unpinned batch is still capped.
|
||||||
|
c = newTestConn(1, 8)
|
||||||
|
if got := c.batchCount(1024 * 1024); got != 1 {
|
||||||
|
t.Fatalf("unpinned batch should be capped to 1, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdaptiveBatchStopsAtFloor(t *testing.T) {
|
||||||
|
c := newTestConn(4, 32)
|
||||||
|
seen := map[int]bool{}
|
||||||
|
for i := 0; i < 12; i++ {
|
||||||
|
_, next, _ := c.shrinkPipeline()
|
||||||
|
seen[next] = true
|
||||||
|
}
|
||||||
|
if c.pipeline != 4 {
|
||||||
|
t.Fatalf("batch fell to %d, want the floor 4", c.pipeline)
|
||||||
|
}
|
||||||
|
if !seen[16] || !seen[8] {
|
||||||
|
t.Fatalf("expected halving through 16 and 8, saw %v", seen)
|
||||||
|
}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
c.growPipeline()
|
||||||
|
}
|
||||||
|
if c.pipeline != 32 {
|
||||||
|
t.Fatalf("batch grew to %d, want the ceiling 32", c.pipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleBatchIsFixed(t *testing.T) {
|
||||||
|
c := newTestConn(1, 1)
|
||||||
|
if !c.pinnedBatch() {
|
||||||
|
t.Fatal("a 1..1 batch must be treated as pinned")
|
||||||
|
}
|
||||||
|
c.growPipeline()
|
||||||
|
if c.pipeline != 1 {
|
||||||
|
t.Fatalf("batch of 1 grew to %d", c.pipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReconnectZeroMeansPersistent(t *testing.T) {
|
func TestReconnectZeroMeansPersistent(t *testing.T) {
|
||||||
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
|
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
|
||||||
if lane.reconnectEvery != 0 {
|
if lane.reconnectEvery != 0 {
|
||||||
|
|||||||
@@ -374,7 +374,8 @@ func main() {
|
|||||||
chunkAdaptLog = flag.Bool("chunk-adapt-log", true, "print adaptive chunk size changes")
|
chunkAdaptLog = flag.Bool("chunk-adapt-log", true, "print adaptive chunk size changes")
|
||||||
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
|
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
|
||||||
chunkPollers = flag.Int("chunk-pollers", 1, "reserved compatibility setting; binary transport uses one download worker")
|
chunkPollers = flag.Int("chunk-pollers", 1, "reserved compatibility setting; binary transport uses one download worker")
|
||||||
chunkConcurrency = flag.Int("chunk-concurrency", 1, "maximum adaptive download pipeline depth (1-256); 1 keeps concurrency fixed at one")
|
chunkConcurrency = flag.Int("chunk-concurrency", 1, "maximum download records per request (1-256)")
|
||||||
|
chunkConcurrencyMin = flag.Int("chunk-concurrency-min", 1, "minimum download records per request (1-256); equal to --chunk-concurrency pins the depth and disables batch adaptation")
|
||||||
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "force reconnect after N logical requests; 0 = persistent/automatic")
|
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "force reconnect after N logical requests; 0 = persistent/automatic")
|
||||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||||
@@ -417,6 +418,14 @@ func main() {
|
|||||||
fmt.Fprintln(os.Stderr, "--chunk-concurrency must be between 1 and 256")
|
fmt.Fprintln(os.Stderr, "--chunk-concurrency must be between 1 and 256")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
if *chunkConcurrencyMin < 1 || *chunkConcurrencyMin > 256 {
|
||||||
|
fmt.Fprintln(os.Stderr, "--chunk-concurrency-min must be between 1 and 256")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if *chunkConcurrencyMin > *chunkConcurrency {
|
||||||
|
fmt.Fprintln(os.Stderr, "--chunk-concurrency-min must not exceed --chunk-concurrency")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
if *chunkReconnect < 0 {
|
if *chunkReconnect < 0 {
|
||||||
fmt.Fprintln(os.Stderr, "--chunk-reconnect-every must be 0 or greater")
|
fmt.Fprintln(os.Stderr, "--chunk-reconnect-every must be 0 or greater")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@@ -433,6 +442,7 @@ func main() {
|
|||||||
pollDelay: *chunkPollDelay,
|
pollDelay: *chunkPollDelay,
|
||||||
txnTimeout: *chunkTimeout,
|
txnTimeout: *chunkTimeout,
|
||||||
tcpBuffer: *tcpBuffer,
|
tcpBuffer: *tcpBuffer,
|
||||||
|
minPipeline: *chunkConcurrencyMin,
|
||||||
maxPipeline: *chunkConcurrency,
|
maxPipeline: *chunkConcurrency,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,15 +460,21 @@ func main() {
|
|||||||
fmt.Printf("remote DragonTCP endpoint=%s\n", serverAddr)
|
fmt.Printf("remote DragonTCP endpoint=%s\n", serverAddr)
|
||||||
fmt.Printf("max_connections=%d transport=%s tcp_buffer=%d\n", *maxConnections, *transport, *tcpBuffer)
|
fmt.Printf("max_connections=%d transport=%s tcp_buffer=%d\n", *maxConnections, *transport, *tcpBuffer)
|
||||||
if *transport == "chunk" {
|
if *transport == "chunk" {
|
||||||
|
batchMode := "adaptive"
|
||||||
|
if *chunkConcurrencyMin == *chunkConcurrency {
|
||||||
|
batchMode = "pinned"
|
||||||
|
}
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d pollers=%d concurrency=%d reconnect_every=%d timeout=%s\n",
|
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d pollers=%d batch=%d-%d(%s) reconnect_every=%d timeout=%s\n",
|
||||||
*chunkAdaptive,
|
*chunkAdaptive,
|
||||||
*chunkStart,
|
*chunkStart,
|
||||||
*chunkMin,
|
*chunkMin,
|
||||||
*chunkMax,
|
*chunkMax,
|
||||||
*chunkSuccesses,
|
*chunkSuccesses,
|
||||||
*chunkPollers,
|
*chunkPollers,
|
||||||
|
*chunkConcurrencyMin,
|
||||||
*chunkConcurrency,
|
*chunkConcurrency,
|
||||||
|
batchMode,
|
||||||
*chunkReconnect,
|
*chunkReconnect,
|
||||||
chunkTimeout.String(),
|
chunkTimeout.String(),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user