With UDP
This commit is contained in:
@@ -1,168 +1,291 @@
|
||||
DragonTCP Proxy v6 - Server Debug Build
|
||||
=================================
|
||||
# DragonTCP Full Android VPN v1
|
||||
|
||||
This build adds server-side diagnostic logging for the adaptive/chunk transport.
|
||||
The wire protocol and v6 client remain compatible.
|
||||
This build replaces the old HTTP-proxy-only Android design with a real layer-3
|
||||
VPN packet tunnel.
|
||||
|
||||
New server flags
|
||||
----------------
|
||||
It does **not** use the uploaded `jni.zip` and does not depend on HEV or any
|
||||
other tun2socks binary. The Android `VpnService` TUN file descriptor is passed
|
||||
directly to the DragonTCP Go core with Unix `SCM_RIGHTS`, and the Go core moves
|
||||
raw IPv4/IPv6 packets through DragonTCP's adaptive, XOR-framed TCP/53
|
||||
transport.
|
||||
|
||||
--debug
|
||||
Session/connect/error logging plus periodic aggregate statistics.
|
||||
## Architecture
|
||||
|
||||
--debug-chunks
|
||||
Logs every COPEN, CPUSH, ACK, CPULL, DATA, WAIT, EOF, and CCLOSE event.
|
||||
This is extremely verbose with 32-byte chunks and can reduce throughput.
|
||||
Enabling --debug-chunks also enables normal debug logging.
|
||||
```text
|
||||
Android apps
|
||||
|
|
||||
| IPv4 + IPv6 default routes
|
||||
v
|
||||
Android VpnService TUN (MTU 1280)
|
||||
|
|
||||
v
|
||||
DragonTCP Go VPN core
|
||||
|
|
||||
| adaptive small records, XOR 0xAD, TCP/53
|
||||
v
|
||||
DragonTCP VPN server
|
||||
|
|
||||
v
|
||||
Linux TUN dragontcp0
|
||||
|
|
||||
| IP forwarding + NAT
|
||||
v
|
||||
Internet
|
||||
```
|
||||
|
||||
--debug-stats-interval DURATION
|
||||
Aggregate statistics frequency. Default: 5s.
|
||||
Set to 0 to disable periodic statistics.
|
||||
Because complete IP packets are tunneled, this carries TCP, UDP, DNS, ICMP,
|
||||
IPv4 and IPv6. Applications do not need HTTP or SOCKS proxy support.
|
||||
|
||||
Recommended diagnostic command for fixed 32-byte chunks
|
||||
--------------------------------------------------------
|
||||
## Included files
|
||||
|
||||
sudo ./dragontcp-server-linux-amd64 --token 'YOUR_SECRET' --chunk-max 32 --chunk-buffered 2048 --chunk-poll-wait 50ms --chunk-session-timeout 5m --max-connections 20000 --tcp-buffer 0 --debug --debug-chunks --debug-stats-interval 5s
|
||||
```text
|
||||
bin/dragontcp-vpn-server-linux-amd64
|
||||
bin/dragontcp-vpn-server-linux-arm64
|
||||
android/build/DragonTCP-VPN.apk
|
||||
android/lib/arm64-v8a/libdragontcp_vpn.so
|
||||
core/ complete Go source
|
||||
android/src/ complete Android Java source
|
||||
build_core.sh
|
||||
build_all.sh
|
||||
android/build_apk.sh
|
||||
```
|
||||
|
||||
Normal production command with useful low-overhead debug
|
||||
---------------------------------------------------------
|
||||
## Server requirements
|
||||
|
||||
sudo ./dragontcp-server-linux-amd64 --token 'YOUR_SECRET' --chunk-max 32 --chunk-buffered 2048 --chunk-poll-wait 50ms --chunk-session-timeout 5m --max-connections 20000 --tcp-buffer 0 --debug --debug-stats-interval 10s
|
||||
The full VPN server needs root/CAP_NET_ADMIN because it creates a Linux TUN
|
||||
interface and enables packet forwarding/NAT.
|
||||
|
||||
Disable all debug logging
|
||||
-------------------------
|
||||
Install the normal Linux networking tools if they are not already present:
|
||||
|
||||
Simply omit --debug and --debug-chunks.
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y iproute2 iptables
|
||||
```
|
||||
|
||||
Example debug output
|
||||
--------------------
|
||||
TCP port 53 must be free.
|
||||
|
||||
[DEBUG] SESSION OPEN id=... target=example.com:443 max_chunk=32 active_sessions=1
|
||||
[CHUNK] CPUSH id=... seq=0 bytes=32 -> ACK accepted=32
|
||||
[CHUNK] CPULL id=... ack=-1 want=0 offset=0 limit=32
|
||||
[CHUNK] DATA id=... seq=0 offset=0 bytes=32 total=32
|
||||
[CHUNK] CPULL id=... want=8 -> WAIT
|
||||
[DEBUG] SESSION CLOSE id=... active_sessions=0
|
||||
[DEBUG] STATS uptime=10s active_connections=8 active_sessions=2 sessions_opened=5 sessions_closed=3 bytes_up=... bytes_down=... push_records=... pull_requests=... data_records=... waits=... errors=0
|
||||
Check:
|
||||
|
||||
Counters
|
||||
--------
|
||||
```bash
|
||||
sudo ss -lntp | grep ':53'
|
||||
```
|
||||
|
||||
active_connections - currently open DragonTCP TCP connections
|
||||
active_sessions - currently open chunk proxy sessions
|
||||
sessions_opened - total chunk sessions opened
|
||||
sessions_closed - total chunk sessions closed
|
||||
bytes_up - bytes accepted from client and written toward target
|
||||
bytes_down - bytes read from target into chunk buffering
|
||||
push_records - accepted upload CPUSH records
|
||||
pull_requests - CPULL requests received
|
||||
data_records - DATA responses generated
|
||||
waits - WAIT responses because downstream data was not ready yet
|
||||
errors - debug-counted server/protocol errors
|
||||
## Start the server
|
||||
|
||||
Important performance note
|
||||
--------------------------
|
||||
```bash
|
||||
sudo ./dragontcp-vpn-server-linux-amd64 \
|
||||
--token 'YOUR_SECRET' \
|
||||
--debug
|
||||
```
|
||||
|
||||
At 32 bytes, --debug-chunks can generate thousands or millions of log lines for
|
||||
large transfers. Use it while diagnosing a failure, then switch to --debug only
|
||||
for normal use.
|
||||
The defaults are:
|
||||
|
||||
Large-chunk update
|
||||
==================
|
||||
```text
|
||||
listen 0.0.0.0:53/TCP
|
||||
TUN dragontcp0
|
||||
TUN MTU 1280
|
||||
server IPv4 10.123.0.1/16
|
||||
server IPv6 fd7a:4472:6167:6f6e::1/64
|
||||
maximum fragment 65535 bytes
|
||||
poll wait 100ms
|
||||
auto NAT enabled
|
||||
private targets blocked
|
||||
```
|
||||
|
||||
This is the v6 debug/adaptive-chunk branch with FIXED poller concurrency.
|
||||
It intentionally does NOT include the later adaptive-poller controller.
|
||||
The server automatically enables IPv4/IPv6 forwarding and installs
|
||||
MASQUERADE/forward rules with `iptables`/`ip6tables` when available.
|
||||
|
||||
Chunk limits
|
||||
------------
|
||||
If you manage routing/NAT yourself:
|
||||
|
||||
Previous hard limit:
|
||||
```bash
|
||||
sudo ./dragontcp-vpn-server-linux-amd64 \
|
||||
--token 'YOUR_SECRET' \
|
||||
--auto-nat=false
|
||||
```
|
||||
|
||||
8192 bytes
|
||||
To allow clients to reach private/LAN destination addresses too:
|
||||
|
||||
New hard limit:
|
||||
```bash
|
||||
--allow-private
|
||||
```
|
||||
|
||||
1048576 bytes (1 MiB)
|
||||
## Debug server
|
||||
|
||||
The framed protocol ceiling was increased to 2 MiB so a 1 MiB CPUSH/DATA
|
||||
record plus protocol metadata fits safely.
|
||||
Normal diagnostics:
|
||||
|
||||
New defaults:
|
||||
```bash
|
||||
sudo ./dragontcp-vpn-server-linux-amd64 \
|
||||
--token 'YOUR_SECRET' \
|
||||
--debug \
|
||||
--debug-stats-interval 5s
|
||||
```
|
||||
|
||||
client --chunk-max 65536
|
||||
server --chunk-max 65536
|
||||
Very verbose per-IP-packet diagnostics:
|
||||
|
||||
The adaptive client still begins at:
|
||||
```bash
|
||||
--debug-packets
|
||||
```
|
||||
|
||||
--chunk-start 256
|
||||
Do not leave `--debug-packets` enabled for high-throughput use.
|
||||
|
||||
and can grow toward the configured maximum after successful records.
|
||||
## Android app
|
||||
|
||||
Use up to 1 MiB adaptive chunks
|
||||
--------------------------------
|
||||
Install:
|
||||
|
||||
Server:
|
||||
```text
|
||||
DragonTCP-VPN.apk
|
||||
```
|
||||
|
||||
sudo ./dragontcp-server-linux-amd64 --token 'YOUR_SECRET' --chunk-max 1048576 --debug --debug-stats-interval 10s
|
||||
The UI is intentionally small:
|
||||
|
||||
Android ARM64 client with fixed poller count of 8:
|
||||
```text
|
||||
Server
|
||||
TCP Port
|
||||
Token
|
||||
Maximum fragment
|
||||
Minimum fragment
|
||||
Timeout
|
||||
|
||||
./dragontcp-client-android-arm64 --server-host YOUR_SERVER_IP --token 'YOUR_SECRET' --chunk-start 256 --chunk-min 32 --chunk-max 1048576 --chunk-pollers 8 --chunk-timeout 2s --chunk-adapt-log
|
||||
CONNECT
|
||||
STOP
|
||||
|
||||
The number of pollers stays exactly at the value passed with --chunk-pollers.
|
||||
Only the chunk size adapts.
|
||||
Live log
|
||||
```
|
||||
|
||||
Examples of useful ceilings
|
||||
---------------------------
|
||||
Defaults:
|
||||
|
||||
--chunk-max 16384 # 16 KiB
|
||||
--chunk-max 32768 # 32 KiB
|
||||
--chunk-max 65536 # 64 KiB (new default maximum)
|
||||
--chunk-max 131072 # 128 KiB
|
||||
--chunk-max 262144 # 256 KiB
|
||||
--chunk-max 524288 # 512 KiB
|
||||
--chunk-max 1048576 # 1 MiB hard maximum
|
||||
```text
|
||||
Port 53
|
||||
Max 1280
|
||||
Min 32
|
||||
Timeout 2s
|
||||
Pollers 1 (fixed)
|
||||
MTU 1280 (fixed)
|
||||
```
|
||||
|
||||
Fixed-size mode also supports the same range:
|
||||
The starting DragonTCP record size is always the configured maximum. On a
|
||||
transport failure the client automatically reduces it. With Max=1280 and
|
||||
Min=32 the reduction path can converge approximately as:
|
||||
|
||||
--chunk-size 262144
|
||||
```text
|
||||
1280 -> 640 -> 320 -> 160 -> 80 -> 40 -> 32
|
||||
```
|
||||
|
||||
Memory note
|
||||
-----------
|
||||
After sustained successful full-size records it cautiously grows again.
|
||||
|
||||
Larger server chunk maxima require larger per-session target-read buffers and
|
||||
can increase buffered memory substantially when many sessions are active.
|
||||
For thousands of simultaneous users, do not automatically use 1 MiB unless
|
||||
measurements show that it is useful. Values such as 16-64 KiB are a more
|
||||
reasonable starting point, while the adaptive client can still be configured
|
||||
to probe higher when your network supports it.
|
||||
The app assigns itself a stable private DragonTCP VPN IPv4/IPv6 pair on first
|
||||
run. The DragonTCP app UID itself is excluded from the VPN so the TCP/53
|
||||
transport cannot recursively enter its own TUN interface.
|
||||
|
||||
Validation
|
||||
----------
|
||||
## Why Max defaults to 1280
|
||||
|
||||
The updated source and binaries were rebuilt from this v6 debug branch.
|
||||
Validation included:
|
||||
This version transports IP packets, not an HTTP byte stream. The Android VPN
|
||||
MTU is 1280, so an individual IP packet normally cannot exceed 1280 bytes.
|
||||
The UI still accepts larger DragonTCP record ceilings up to 65535, but there
|
||||
is usually no throughput benefit unless the VPN MTU is raised too.
|
||||
|
||||
* Go builds for Linux amd64, Linux ARM64, Linux ARMv7 client, and Android ARM64.
|
||||
* A protocol round-trip test with a full 1 MiB request and response frame.
|
||||
* An 8 MiB HTTP download through the proxy using fixed 262144-byte (256 KiB)
|
||||
chunk configuration; the downloaded SHA-256 matched the source exactly.
|
||||
## Building everything from source
|
||||
|
||||
DragonTCP branding update
|
||||
=========================
|
||||
Requirements:
|
||||
|
||||
This package was renamed from HOX to DragonTCP.
|
||||
- Go 1.22+
|
||||
- JDK 17+
|
||||
- Android SDK platform and build-tools
|
||||
- `zip`
|
||||
|
||||
Binary names are now:
|
||||
No Android NDK is required in this build.
|
||||
|
||||
dragontcp-server-linux-amd64
|
||||
dragontcp-server-arm64
|
||||
dragontcp-client-linux-amd64
|
||||
dragontcp-client-android-arm64
|
||||
dragontcp-client-arm64
|
||||
dragontcp-client-armv7
|
||||
Set the SDK path:
|
||||
|
||||
The Go module and command directories were also renamed to DragonTCP.
|
||||
The existing UP/OK wire framing and chunk protocol were intentionally kept
|
||||
unchanged, so this branding change does not break compatibility with the
|
||||
previous protocol implementation.
|
||||
```bash
|
||||
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
|
||||
```
|
||||
|
||||
Build server, Android native core, and APK:
|
||||
|
||||
```bash
|
||||
./build_all.sh
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
```text
|
||||
bin/dragontcp-vpn-server-linux-amd64
|
||||
bin/dragontcp-vpn-server-linux-arm64
|
||||
android/lib/arm64-v8a/libdragontcp_vpn.so
|
||||
android/build/DragonTCP-VPN.apk
|
||||
```
|
||||
|
||||
Build only Go/native components:
|
||||
|
||||
```bash
|
||||
./build_core.sh
|
||||
```
|
||||
|
||||
Build only APK after the core is present:
|
||||
|
||||
```bash
|
||||
./android/build_apk.sh
|
||||
```
|
||||
|
||||
## Android TUN fd handoff
|
||||
|
||||
The Android service creates the VPN using `VpnService.Builder.establish()`.
|
||||
It then sends that TUN file descriptor to the Go child over a private Unix
|
||||
socket using Android `LocalSocket.setFileDescriptorsForSend()`. The Go side
|
||||
receives the descriptor with `SCM_RIGHTS` and directly reads/writes IP
|
||||
packets.
|
||||
|
||||
This avoids JNI and avoids passing an inherited descriptor through
|
||||
`ProcessBuilder`.
|
||||
|
||||
## Protocol packet mode
|
||||
|
||||
Packet mode still uses the DragonTCP request/response envelope:
|
||||
|
||||
```text
|
||||
request : UP + request-id + length + XOR(payload)
|
||||
response : OK + request-id + length + XOR(payload)
|
||||
```
|
||||
|
||||
The VPN payload protocol is binary rather than text to reduce overhead on very
|
||||
small records.
|
||||
|
||||
Commands include:
|
||||
|
||||
```text
|
||||
VOPEN
|
||||
VPUSH fragment
|
||||
VPULL fragment
|
||||
VCLOSE
|
||||
```
|
||||
|
||||
A random 128-bit session ID is used after authenticated session creation.
|
||||
Packets and fragments have sequence/offset fields so retries do not duplicate
|
||||
bytes.
|
||||
|
||||
## Test mode
|
||||
|
||||
For protocol testing without root/TUN/NAT, the server has:
|
||||
|
||||
```bash
|
||||
./dragontcp-vpn-server-linux-amd64 \
|
||||
--host 127.0.0.1 \
|
||||
--port 19053 \
|
||||
--token test \
|
||||
--mock-echo
|
||||
```
|
||||
|
||||
This echoes complete IP packets back to the client instead of forwarding them
|
||||
to the Internet.
|
||||
|
||||
During development the packet path was tested with IPv4 and IPv6 1280-byte
|
||||
packets while the server forced a 32-byte maximum DragonTCP fragment. Both
|
||||
were reassembled byte-for-byte correctly.
|
||||
|
||||
## Security
|
||||
|
||||
XOR 0xAD remains protocol obfuscation, not cryptographic encryption. HTTPS
|
||||
and other TLS-based application protocols retain their own end-to-end
|
||||
security, but the DragonTCP transport itself should not be considered
|
||||
cryptographically confidential.
|
||||
|
||||
+4
-6
@@ -1,6 +1,4 @@
|
||||
1f1058a03099fc04dd00c727656f26a88ebcf43f7ef924d11ecb5675aab96895 bin/dragontcp-client-android-arm64
|
||||
232c95474d90c009f18db9c2d870c910a8248f97185b715e54166eab4baeaebf bin/dragontcp-client-arm64
|
||||
759fc70875d3501c9954e9db99a2b6f51d1ff9f4f39ff3d9b21062b971890e3e bin/dragontcp-client-armv7
|
||||
3b18a4b7dc79819b22ef550b1761c8f36b51793bec35ff077d038c1a7d00ee79 bin/dragontcp-client-linux-amd64
|
||||
4a00acbc2fdeaa1acd34c528b84813c3688a03134d591bed34f94254a46d8d06 bin/dragontcp-server-arm64
|
||||
0bc46c805ceabad0dd149f8b068f98e75eba8cd08b86c9298dc7f34094e9c666 bin/dragontcp-server-linux-amd64
|
||||
1c854f81ee4d493c7e7b5956f7a81d08a4019e6a1ba9aef00ca64df702cdc90f android/build/DragonTCP-VPN.apk
|
||||
a05c475d98b922c053142cd2f65b52c22facd8404370108671010846c26019bc bin/dragontcp-vpn-server-linux-amd64
|
||||
29d52797045da9d13114264187bfe7445f78f3ef4fac225d29aec9348efb1be1 bin/dragontcp-vpn-server-linux-arm64
|
||||
5d8282fd03af2178a3061711edba177ab144db50a0aeaa170d53598ff795fb07 android/lib/arm64-v8a/libdragontcp_vpn.so
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.dragontcp.client"
|
||||
android:versionCode="3"
|
||||
android:versionName="2.0">
|
||||
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<application android:allowBackup="false" android:extractNativeLibs="true" android:label="DragonTCP VPN" android:usesCleartextTraffic="true">
|
||||
<activity android:name=".MainActivity" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity>
|
||||
<service android:name=".DragonService" android:exported="true" android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter><action android:name="android.net.VpnService"/></intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
SDK="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
|
||||
[[ -n "$SDK" ]] || { echo "Set ANDROID_SDK_ROOT" >&2; exit 1; }
|
||||
BUILD_TOOLS="${BUILD_TOOLS:-35.0.0}"; PLATFORM="${PLATFORM:-android-35}"
|
||||
if [[ ! -d "$SDK/build-tools/$BUILD_TOOLS" ]]; then BUILD_TOOLS="$(find "$SDK/build-tools" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -V | tail -1)"; fi
|
||||
if [[ ! -f "$SDK/platforms/$PLATFORM/android.jar" ]]; then PLATFORM="$(find "$SDK/platforms" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -V | tail -1)"; fi
|
||||
BT="$SDK/build-tools/$BUILD_TOOLS"; AJ="$SDK/platforms/$PLATFORM/android.jar"
|
||||
for tool in aapt d8 apksigner; do [[ -x "$BT/$tool" ]] || { echo "Missing $BT/$tool" >&2; exit 1; }; done
|
||||
CORE="$ROOT/lib/arm64-v8a/libdragontcp_vpn.so"; [[ -f "$CORE" ]] || { echo "Run ../build_core.sh first" >&2; exit 1; }
|
||||
B="$ROOT/build"; rm -rf "$B"; mkdir -p "$B/classes" "$B/dex"
|
||||
"$BT/aapt" package -f -M "$ROOT/AndroidManifest.xml" -S "$ROOT/res" -I "$AJ" -F "$B/resources.ap_"
|
||||
javac -source 8 -target 8 -classpath "$AJ" -d "$B/classes" $(find "$ROOT/src" -name '*.java' -print)
|
||||
"$BT/d8" --lib "$AJ" --min-api 29 --output "$B/dex" $(find "$B/classes" -name '*.class' -print)
|
||||
cp "$B/resources.ap_" "$B/DragonTCP-VPN-unsigned.apk"
|
||||
(cd "$B/dex" && zip -q "$B/DragonTCP-VPN-unsigned.apk" classes.dex)
|
||||
(cd "$ROOT" && zip -q -r "$B/DragonTCP-VPN-unsigned.apk" lib)
|
||||
KEYSTORE="$ROOT/dragontcp-debug.jks"
|
||||
if [[ ! -f "$KEYSTORE" ]]; then keytool -genkeypair -keystore "$KEYSTORE" -storepass dragontcp -keypass dragontcp -alias dragontcp -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=DragonTCP VPN,O=DragonTCP,C=US"; fi
|
||||
"$BT/apksigner" sign --ks "$KEYSTORE" --ks-pass pass:dragontcp --key-pass pass:dragontcp --out "$B/DragonTCP-VPN.apk" "$B/DragonTCP-VPN-unsigned.apk"
|
||||
"$BT/apksigner" verify --verbose "$B/DragonTCP-VPN.apk"
|
||||
echo "Built APK: $B/DragonTCP-VPN.apk"
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<resources><string name="app_name">DragonTCP VPN</string></resources>
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DragonService extends VpnService {
|
||||
public static final String ACTION_CONNECT="com.dragontcp.client.CONNECT";
|
||||
public static final String ACTION_STOP="com.dragontcp.client.STOP";
|
||||
public static volatile boolean active=false,running=false;
|
||||
public static volatile String state="Stopped";
|
||||
private static final String CHANNEL_ID="dragontcp_vpn";
|
||||
private static final int NOTIFICATION_ID=53;
|
||||
private final Object lifecycleLock=new Object();
|
||||
private Process process;
|
||||
private Thread outputThread;
|
||||
private ParcelFileDescriptor vpnInterface;
|
||||
private File fdSocketFile;
|
||||
|
||||
@Override public void onCreate(){super.onCreate();createNotificationChannel();}
|
||||
@Override public int onStartCommand(Intent intent,int flags,int startId){
|
||||
if(intent==null)return START_NOT_STICKY;String action=intent.getAction();
|
||||
if(ACTION_STOP.equals(action)){appendLog("STOP requested");shutdown("Stopped by user",true);return START_NOT_STICKY;}
|
||||
if(!ACTION_CONNECT.equals(action))return START_NOT_STICKY;
|
||||
cleanupResources(true);clearLog();active=true;running=false;state="Starting VPN";startForeground(NOTIFICATION_ID,buildNotification("Starting full VPN"));
|
||||
String server=intent.getStringExtra("server"),token=intent.getStringExtra("token"),timeout=intent.getStringExtra("timeout"),v4=intent.getStringExtra("vpnIPv4"),v6=intent.getStringExtra("vpnIPv6");
|
||||
int port=intent.getIntExtra("port",53),max=intent.getIntExtra("chunkMax",1280),min=intent.getIntExtra("chunkMin",32),start=intent.getIntExtra("chunkStart",max);
|
||||
if(server==null||server.trim().isEmpty()){failStart("Server is empty");return START_NOT_STICKY;}if(token==null)token="";if(timeout==null||timeout.isEmpty())timeout="2s";if(v4==null||v6==null){failStart("Missing VPN client address");return START_NOT_STICKY;}
|
||||
start=max;
|
||||
try{
|
||||
establishPacketVpn(v4,v6);
|
||||
state="Starting DragonTCP core";
|
||||
startCore(server.trim(),port,token,start,min,max,timeout.trim(),v4,v6);
|
||||
state="Connecting to DragonTCP server";
|
||||
updateNotification("Connecting • TCP/"+port);
|
||||
}catch(Exception e){failStart(e.getMessage()==null?e.toString():e.getMessage());}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void establishPacketVpn(String v4,String v6)throws Exception{
|
||||
VpnService.Builder b=new VpnService.Builder();b.setSession("DragonTCP VPN");b.setMtu(1280);
|
||||
b.addAddress(v4,32);b.addAddress(v6,128);b.addRoute("0.0.0.0",0);b.addRoute("::",0);
|
||||
b.addDnsServer("1.1.1.1");b.addDnsServer("2606:4700:4700::1111");
|
||||
try{b.addDisallowedApplication(getPackageName());}catch(PackageManager.NameNotFoundException e){throw new Exception("Cannot exclude DragonTCP from its own VPN",e);}
|
||||
Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent pi=PendingIntent.getActivity(this,1,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);b.setConfigureIntent(pi);
|
||||
vpnInterface=b.establish();if(vpnInterface==null)throw new Exception("Android did not establish the TUN interface");
|
||||
appendLog("TUN established: "+v4+" + "+v6+" MTU=1280");appendLog("Routes captured: 0.0.0.0/0 and ::/0");appendLog("DNS through VPN: 1.1.1.1 + 2606:4700:4700::1111");appendLog("DragonTCP app UID excluded from VPN to prevent recursion");
|
||||
}
|
||||
|
||||
private void startCore(String server,int port,String token,int start,int min,int max,String timeout,String v4,String v6)throws Exception{
|
||||
String executable=getApplicationInfo().nativeLibraryDir+"/libdragontcp_vpn.so";File exe=new File(executable);if(!exe.exists())throw new Exception("Embedded DragonTCP VPN core was not extracted");
|
||||
fdSocketFile=new File(getFilesDir(),"dragontcp-tunfd.sock");if(fdSocketFile.exists())fdSocketFile.delete();
|
||||
List<String> cmd=new ArrayList<String>();cmd.add(executable);cmd.add("--server-host");cmd.add(server);cmd.add("--server-port");cmd.add(String.valueOf(port));cmd.add("--token");cmd.add(token);
|
||||
cmd.add("--tun-fd-socket");cmd.add(fdSocketFile.getAbsolutePath());cmd.add("--vpn-ipv4");cmd.add(v4);cmd.add("--vpn-ipv6");cmd.add(v6);cmd.add("--vpn-mtu");cmd.add("1280");
|
||||
cmd.add("--chunk-start");cmd.add(String.valueOf(max));cmd.add("--chunk-max");cmd.add(String.valueOf(max));cmd.add("--chunk-min");cmd.add(String.valueOf(min));cmd.add("--chunk-grow-after");cmd.add("64");cmd.add("--chunk-timeout");cmd.add(timeout);cmd.add("--chunk-reconnect-every");cmd.add("32");cmd.add("--chunk-adapt-log");
|
||||
appendLog("Server: "+server+":"+port);appendLog("Transport chunks: start=max="+max+" min="+min+" pollers=1 timeout="+timeout);
|
||||
ProcessBuilder pb=new ProcessBuilder(cmd);pb.redirectErrorStream(true);pb.directory(getFilesDir());final Process p=pb.start();synchronized(lifecycleLock){process=p;}
|
||||
outputThread=new Thread(()->readCoreOutput(p),"DragonTCP-output");outputThread.setDaemon(true);outputThread.start();
|
||||
passTunFdWhenReady();
|
||||
}
|
||||
|
||||
private void passTunFdWhenReady()throws Exception{
|
||||
long deadline=System.currentTimeMillis()+5000;while(System.currentTimeMillis()<deadline){if(fdSocketFile!=null&&fdSocketFile.exists())break;Process p; synchronized(lifecycleLock){p=process;}if(p==null||!p.isAlive())throw new Exception("DragonTCP core exited before TUN handoff");Thread.sleep(25);}
|
||||
if(fdSocketFile==null||!fdSocketFile.exists())throw new Exception("DragonTCP core did not create its TUN-fd socket");
|
||||
LocalSocket s=new LocalSocket();try{s.connect(new LocalSocketAddress(fdSocketFile.getAbsolutePath(),LocalSocketAddress.Namespace.FILESYSTEM));FileDescriptor fd=vpnInterface.getFileDescriptor();s.setFileDescriptorsForSend(new FileDescriptor[]{fd});s.getOutputStream().write(0x44);s.getOutputStream().flush();appendLog("TUN file descriptor passed to DragonTCP core");}finally{try{s.close();}catch(Exception ignored){}}
|
||||
}
|
||||
|
||||
private void readCoreOutput(Process p){
|
||||
try{BufferedReader br=new BufferedReader(new InputStreamReader(p.getInputStream()));String line;while((line=br.readLine())!=null){appendLog(line);if(line.contains("VPN READY")){running=true;active=true;state="Connected • Full VPN";updateNotification("Connected • IPv4 + IPv6 • TCP/UDP");}}
|
||||
int code=p.waitFor();handleCoreExit(p,code);
|
||||
}catch(Exception e){appendLog("Core reader: "+e);handleCoreExit(p,-1);}
|
||||
}
|
||||
private void handleCoreExit(Process p,int code){boolean owns; synchronized(lifecycleLock){owns=process==p;if(owns)process=null;}if(!owns)return;appendLog("DragonTCP core exited: "+code);running=false;active=false;state="Core exited ("+code+")";closeVpn();stopForeground(true);stopSelf();}
|
||||
|
||||
private Notification buildNotification(String msg){Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);PendingIntent op=PendingIntent.getActivity(this,0,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Intent stop=new Intent(this,DragonService.class);stop.setAction(ACTION_STOP);PendingIntent sp=PendingIntent.getService(this,2,stop,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Notification.Builder nb=Build.VERSION.SDK_INT>=26?new Notification.Builder(this,CHANNEL_ID):new Notification.Builder(this);return nb.setContentTitle("DragonTCP VPN").setContentText(msg).setSmallIcon(android.R.drawable.stat_sys_upload).setOngoing(true).setContentIntent(op).addAction(android.R.drawable.ic_menu_close_clear_cancel,"STOP",sp).build();}
|
||||
private void updateNotification(String m){NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.notify(NOTIFICATION_ID,buildNotification(m));}
|
||||
private void createNotificationChannel(){if(Build.VERSION.SDK_INT>=26){NotificationChannel c=new NotificationChannel(CHANNEL_ID,"DragonTCP VPN",NotificationManager.IMPORTANCE_LOW);c.setDescription("DragonTCP full packet VPN status");NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.createNotificationChannel(c);}}
|
||||
private synchronized void appendLog(String line){try(PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),true),"UTF-8"))){out.println(line);out.flush();}catch(Exception ignored){}}
|
||||
private void clearLog(){try{new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),false).close();}catch(Exception ignored){}}
|
||||
private void failStart(String m){appendLog("START ERROR: "+m);running=false;active=false;state="Start failed: "+m;cleanupResources(true);stopForeground(true);stopSelf();}
|
||||
private void shutdown(String reason,boolean stop){state="Stopping";running=false;appendLog(reason);cleanupResources(true);active=false;state="Stopped";stopForeground(true);if(stop)stopSelf();}
|
||||
private void cleanupResources(boolean kill){Process p; synchronized(lifecycleLock){p=process;process=null;}if(p!=null){try{p.getInputStream().close();}catch(Exception ignored){}try{p.destroy();}catch(Exception ignored){}if(kill){try{if(!p.waitFor(800,TimeUnit.MILLISECONDS)){p.destroyForcibly();p.waitFor(800,TimeUnit.MILLISECONDS);}}catch(Exception ignored){try{p.destroyForcibly();}catch(Exception ignored2){}}}}Thread t=outputThread;outputThread=null;if(t!=null&&t!=Thread.currentThread())t.interrupt();closeVpn();if(fdSocketFile!=null){fdSocketFile.delete();fdSocketFile=null;}running=false;}
|
||||
private void closeVpn(){ParcelFileDescriptor v=vpnInterface;vpnInterface=null;if(v!=null){try{v.close();}catch(Exception ignored){}}}
|
||||
@Override public void onRevoke(){appendLog("VPN permission revoked");shutdown("VPN revoked",true);super.onRevoke();}
|
||||
@Override public void onDestroy(){cleanupResources(true);active=false;running=false;if(!state.startsWith("Start failed")&&!state.startsWith("Core exited"))state="Stopped";stopForeground(true);super.onDestroy();}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.InputType;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
private static final int VPN_REQUEST = 5301;
|
||||
|
||||
private EditText server, port, token, chunkMax, chunkMin, timeout;
|
||||
private TextView status, logs;
|
||||
private ScrollView logScroll;
|
||||
private Button connectButton, stopButton;
|
||||
private Intent pendingServiceIntent;
|
||||
private SharedPreferences prefs;
|
||||
private String lastLogText = "";
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final Runnable refresher = new Runnable() {
|
||||
@Override public void run() {
|
||||
refreshStatus();
|
||||
handler.postDelayed(this, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@Override protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
prefs = getSharedPreferences("dragontcp", MODE_PRIVATE);
|
||||
setTitle("DragonTCP VPN");
|
||||
buildUi();
|
||||
loadSettings();
|
||||
handler.post(refresher);
|
||||
}
|
||||
|
||||
private int dp(int v) { return (int)(v * getResources().getDisplayMetrics().density + 0.5f); }
|
||||
private TextView text(String s, float sp, boolean bold) {
|
||||
TextView v = new TextView(this); v.setText(s); v.setTextSize(sp); v.setTextColor(Color.rgb(232,236,241));
|
||||
if (bold) v.setTypeface(Typeface.DEFAULT, Typeface.BOLD); return v;
|
||||
}
|
||||
private EditText field(LinearLayout root, String label, int type) {
|
||||
TextView t=text(label,13f,false);t.setPadding(0,dp(9),0,dp(4));root.addView(t);
|
||||
EditText e=new EditText(this);e.setSingleLine(true);e.setTextColor(Color.WHITE);e.setHintTextColor(Color.GRAY);e.setInputType(type);
|
||||
e.setBackgroundColor(Color.rgb(42,47,54));e.setPadding(dp(12),dp(9),dp(12),dp(9));
|
||||
root.addView(e,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));return e;
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
ScrollView page=new ScrollView(this);page.setFillViewport(true);page.setBackgroundColor(Color.rgb(20,23,27));
|
||||
LinearLayout root=new LinearLayout(this);root.setOrientation(LinearLayout.VERTICAL);root.setPadding(dp(18),dp(18),dp(18),dp(24));page.addView(root);
|
||||
TextView title=text("DragonTCP VPN",27f,true);title.setTextColor(Color.rgb(104,207,255));root.addView(title);
|
||||
TextView sub=text("Full IPv4 / IPv6 packet VPN over adaptive TCP/53",13f,false);sub.setTextColor(Color.rgb(170,179,188));sub.setPadding(0,dp(2),0,dp(12));root.addView(sub);
|
||||
status=text("Stopped",16f,true);status.setPadding(dp(12),dp(12),dp(12),dp(12));status.setBackgroundColor(Color.rgb(34,39,45));root.addView(status);
|
||||
|
||||
server=field(root,"Server IP / hostname",InputType.TYPE_CLASS_TEXT);
|
||||
port=field(root,"TCP port",InputType.TYPE_CLASS_NUMBER);
|
||||
token=field(root,"Token",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
chunkMax=field(root,"Maximum transport fragment bytes (start = max)",InputType.TYPE_CLASS_NUMBER);
|
||||
chunkMin=field(root,"Minimum transport fragment bytes",InputType.TYPE_CLASS_NUMBER);
|
||||
timeout=field(root,"Transaction timeout (example: 2s)",InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
TextView note=text("Pollers are fixed at 1. Adaptive chunks always start at Max and shrink on failures. All IPv4 and IPv6 routes are captured by the VPN; DragonTCP itself is excluded to prevent a tunnel loop.",12f,false);
|
||||
note.setTextColor(Color.rgb(160,170,180));note.setPadding(0,dp(10),0,dp(8));root.addView(note);
|
||||
|
||||
LinearLayout buttons=new LinearLayout(this);buttons.setOrientation(LinearLayout.HORIZONTAL);buttons.setGravity(Gravity.CENTER);buttons.setPadding(0,dp(8),0,dp(10));root.addView(buttons);
|
||||
connectButton=new Button(this);connectButton.setText("CONNECT");buttons.addView(connectButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
stopButton=new Button(this);stopButton.setText("STOP");buttons.addView(stopButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
connectButton.setOnClickListener(v -> startDragon()); stopButton.setOnClickListener(v -> stopDragon());
|
||||
|
||||
LinearLayout lh=new LinearLayout(this);lh.setOrientation(LinearLayout.HORIZONTAL);lh.setGravity(Gravity.CENTER_VERTICAL);root.addView(lh);
|
||||
TextView lt=text("Live log",17f,true);lh.addView(lt,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
Button clear=new Button(this);clear.setText("CLEAR");lh.addView(clear);clear.setOnClickListener(v -> clearLog());
|
||||
logScroll=new ScrollView(this);logScroll.setFillViewport(true);logScroll.setVerticalScrollBarEnabled(true);logScroll.setBackgroundColor(Color.BLACK);
|
||||
logs=text("",11f,false);logs.setTypeface(Typeface.MONOSPACE);logs.setTextIsSelectable(true);logs.setPadding(dp(10),dp(10),dp(10),dp(10));logs.setBackgroundColor(Color.BLACK);
|
||||
logScroll.addView(logs,new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
root.addView(logScroll,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,dp(320)));
|
||||
setContentView(page);
|
||||
}
|
||||
|
||||
private void loadSettings(){server.setText(prefs.getString("server",""));port.setText(prefs.getString("port","53"));token.setText(prefs.getString("token",""));chunkMax.setText(prefs.getString("chunkMax","1280"));chunkMin.setText(prefs.getString("chunkMin","32"));timeout.setText(prefs.getString("timeout","2s"));}
|
||||
private int intValue(EditText e,int d){try{return Integer.parseInt(e.getText().toString().trim());}catch(Exception x){return d;}}
|
||||
private boolean validateSettings(){
|
||||
if(server.getText().toString().trim().isEmpty()){toast("Enter the server IP or hostname");return false;}
|
||||
int p=intValue(port,53),min=intValue(chunkMin,32),max=intValue(chunkMax,1280);
|
||||
if(p<1||p>65535){toast("Port must be 1-65535");return false;}
|
||||
if(min<32||max>65535||min>max){toast("Chunks must satisfy 32 <= Min <= Max <= 65535");return false;}
|
||||
if(timeout.getText().toString().trim().isEmpty()){toast("Enter a timeout such as 2s");return false;}
|
||||
return true;
|
||||
}
|
||||
private void saveSettings(){prefs.edit().putString("server",server.getText().toString().trim()).putString("port",port.getText().toString().trim()).putString("token",token.getText().toString()).putString("chunkMax",chunkMax.getText().toString().trim()).putString("chunkMin",chunkMin.getText().toString().trim()).putString("timeout",timeout.getText().toString().trim()).apply();}
|
||||
|
||||
private int clientHostId(){
|
||||
int id=prefs.getInt("clientHostId",0);if(id>=2&&id<=65534)return id;
|
||||
id=2+new SecureRandom().nextInt(65533);prefs.edit().putInt("clientHostId",id).apply();return id;
|
||||
}
|
||||
private String clientIPv4(int id){return "10.123."+((id>>8)&255)+"."+(id&255);}
|
||||
private String clientIPv6(int id){return "fd7a:4472:6167:6f6e::"+Integer.toHexString(id);}
|
||||
|
||||
private Intent buildServiceIntent(){
|
||||
int max=intValue(chunkMax,1280),id=clientHostId();Intent i=new Intent(this,DragonService.class);i.setAction(DragonService.ACTION_CONNECT);
|
||||
i.putExtra("server",server.getText().toString().trim());i.putExtra("port",intValue(port,53));i.putExtra("token",token.getText().toString());
|
||||
i.putExtra("chunkStart",max);i.putExtra("chunkMax",max);i.putExtra("chunkMin",intValue(chunkMin,32));i.putExtra("timeout",timeout.getText().toString().trim());
|
||||
i.putExtra("vpnIPv4",clientIPv4(id));i.putExtra("vpnIPv6",clientIPv6(id));return i;
|
||||
}
|
||||
private void startDragon(){if(!validateSettings())return;saveSettings();pendingServiceIntent=buildServiceIntent();DragonService.active=true;DragonService.state="Waiting for VPN permission";refreshStatus();Intent prep=VpnService.prepare(this);if(prep!=null)startActivityForResult(prep,VPN_REQUEST);else{Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}}
|
||||
private void launchService(Intent i){if(i==null)return;DragonService.active=true;DragonService.state="Starting full VPN";refreshStatus();if(Build.VERSION.SDK_INT>=26)startForegroundService(i);else startService(i);toast("Starting DragonTCP VPN...");}
|
||||
@Override protected void onActivityResult(int requestCode,int resultCode,Intent data){super.onActivityResult(requestCode,resultCode,data);if(requestCode!=VPN_REQUEST)return;if(resultCode==RESULT_OK&&pendingServiceIntent!=null){Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}else{pendingServiceIntent=null;DragonService.active=false;DragonService.running=false;DragonService.state="VPN permission denied";refreshStatus();toast("VPN permission is required");}}
|
||||
private void stopDragon(){pendingServiceIntent=null;DragonService.state="Stopping...";refreshStatus();Intent s=new Intent(this,DragonService.class);s.setAction(DragonService.ACTION_STOP);try{startService(s);}catch(Exception e){stopService(new Intent(this,DragonService.class));}handler.postDelayed(()->{if(DragonService.active)stopService(new Intent(MainActivity.this,DragonService.class));refreshStatus();},1800);}
|
||||
private void clearLog(){try{File f=new File(getFilesDir(),"dragontcp.log");new java.io.FileOutputStream(f,false).close();lastLogText="";logs.setText("");}catch(Exception e){toast("Could not clear log: "+e.getMessage());}}
|
||||
private String readTail(File f,int maxBytes){if(!f.exists())return "";try(FileInputStream in=new FileInputStream(f)){long len=f.length();int n=(int)Math.min((long)maxBytes,len);byte[]buf=new byte[n];long skip=len-n;while(skip>0){long s=in.skip(skip);if(s<=0)break;skip-=s;}int off=0;while(off<n){int r=in.read(buf,off,n-off);if(r<0)break;off+=r;}return new String(buf,0,off,"UTF-8");}catch(Exception e){return "log error: "+e.getMessage();}}
|
||||
private void refreshStatus(){boolean a=DragonService.active,r=DragonService.running;status.setText((r?"● ":a?"◐ ":"○ ")+DragonService.state);status.setTextColor(r?Color.rgb(115,235,145):a?Color.rgb(255,205,95):Color.rgb(232,236,241));connectButton.setEnabled(!a);stopButton.setEnabled(a);String current=readTail(new File(getFilesDir(),"dragontcp.log"),131072);if(!current.equals(lastLogText)){lastLogText=current;logs.setText(current);logScroll.post(()->logScroll.fullScroll(View.FOCUS_DOWN));}}
|
||||
private void toast(String s){Toast.makeText(this,s,Toast.LENGTH_LONG).show();}
|
||||
@Override protected void onDestroy(){handler.removeCallbacks(refresher);super.onDestroy();}
|
||||
}
|
||||
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.
@@ -0,0 +1,5 @@
|
||||
#!/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 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT/core"
|
||||
mkdir -p "$ROOT/bin" "$ROOT/android/lib/arm64-v8a"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o "$ROOT/bin/dragontcp-vpn-server-linux-amd64" ./cmd/dragontcp-vpn-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o "$ROOT/bin/dragontcp-vpn-server-linux-arm64" ./cmd/dragontcp-vpn-server
|
||||
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o "$ROOT/android/lib/arm64-v8a/libdragontcp_vpn.so" ./cmd/dragontcp-vpn-client
|
||||
echo "Built DragonTCP VPN server + Android core"
|
||||
@@ -0,0 +1,513 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"dragontcpvpn/internal/protocol"
|
||||
)
|
||||
|
||||
var requestCounter atomic.Uint32
|
||||
|
||||
type txnLane struct {
|
||||
mu sync.Mutex
|
||||
serverAddr string
|
||||
timeout time.Duration
|
||||
reconnectEvery int
|
||||
conn net.Conn
|
||||
count int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTxnLane(addr string, timeout time.Duration, reconnectEvery int) *txnLane {
|
||||
return &txnLane{serverAddr: addr, timeout: timeout, reconnectEvery: reconnectEvery}
|
||||
}
|
||||
func (l *txnLane) closeLocked() {
|
||||
if l.conn != nil {
|
||||
_ = l.conn.Close()
|
||||
l.conn = nil
|
||||
}
|
||||
l.count = 0
|
||||
}
|
||||
func (l *txnLane) Close() { l.mu.Lock(); l.closed = true; l.closeLocked(); l.mu.Unlock() }
|
||||
func (l *txnLane) ensureConn() error {
|
||||
if l.closed {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if l.conn != nil && (l.reconnectEvery <= 0 || l.count < l.reconnectEvery) {
|
||||
return nil
|
||||
}
|
||||
l.closeLocked()
|
||||
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
c, err := d.Dial("tcp", l.serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(c)
|
||||
l.conn = c
|
||||
return nil
|
||||
}
|
||||
func (l *txnLane) Do(payload []byte) ([]byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
_ = l.conn.SetDeadline(time.Now().Add(timeout))
|
||||
id := requestCounter.Add(1)
|
||||
if err := protocol.WriteRequestFrame(l.conn, id, payload); err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
rid, resp, err := protocol.ReadResponseFrame(l.conn)
|
||||
if err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
if rid != id {
|
||||
l.closeLocked()
|
||||
return nil, errors.New("request ID mismatch")
|
||||
}
|
||||
l.count++
|
||||
_ = l.conn.SetDeadline(time.Time{})
|
||||
return resp, nil
|
||||
}
|
||||
func doControl(l *txnLane, payload []byte) ([]byte, error) {
|
||||
var last error
|
||||
for i := 0; i < 6; i++ {
|
||||
r, e := l.Do(payload)
|
||||
if e == nil {
|
||||
return r, nil
|
||||
}
|
||||
last = e
|
||||
time.Sleep(time.Duration(i+1) * 50 * time.Millisecond)
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
mu sync.Mutex
|
||||
name string
|
||||
current, min, max int
|
||||
successes int
|
||||
growAfter int
|
||||
log bool
|
||||
}
|
||||
|
||||
func newSizer(name string, start, min, max, growAfter int, log bool) *adaptiveSizer {
|
||||
if min < 32 {
|
||||
min = 32
|
||||
}
|
||||
if max > protocol.VPNMaxFragment {
|
||||
max = protocol.VPNMaxFragment
|
||||
}
|
||||
if max < min {
|
||||
max = min
|
||||
}
|
||||
if start < min {
|
||||
start = min
|
||||
}
|
||||
if start > max {
|
||||
start = max
|
||||
}
|
||||
if growAfter < 1 {
|
||||
growAfter = 32
|
||||
}
|
||||
return &adaptiveSizer{name: name, current: start, min: min, max: max, growAfter: growAfter, log: log}
|
||||
}
|
||||
func (s *adaptiveSizer) Current() int { s.mu.Lock(); v := s.current; s.mu.Unlock(); return v }
|
||||
func (s *adaptiveSizer) Failure(actual int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.current
|
||||
s.successes = 0
|
||||
basis := actual
|
||||
if basis <= 0 || basis > old {
|
||||
basis = old
|
||||
}
|
||||
next := basis / 2
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
if next >= old && old > s.min {
|
||||
next = old / 2
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
}
|
||||
if next < old {
|
||||
s.current = next
|
||||
if s.log {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure (record=%d)\n", s.name, old, next, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *adaptiveSizer) Success(actual int, full bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.current >= s.max || !full {
|
||||
return
|
||||
}
|
||||
s.successes++
|
||||
if s.successes < s.growAfter {
|
||||
return
|
||||
}
|
||||
s.successes = 0
|
||||
old := s.current
|
||||
step := old / 4
|
||||
if step < 32 {
|
||||
step = 32
|
||||
}
|
||||
next := old + step
|
||||
if next > s.max {
|
||||
next = s.max
|
||||
}
|
||||
if next > old {
|
||||
s.current = next
|
||||
if s.log {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after stable success\n", s.name, old, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveTunFD(path string, timeout time.Duration) (*os.File, error) {
|
||||
_ = os.Remove(path)
|
||||
addr := &net.UnixAddr{Name: path, Net: "unix"}
|
||||
ln, err := net.ListenUnix("unix", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { ln.Close(); os.Remove(path) }()
|
||||
_ = os.Chmod(path, 0600)
|
||||
fmt.Printf("TUNFD READY %s\n", path)
|
||||
_ = ln.SetDeadline(time.Now().Add(timeout))
|
||||
c, err := ln.AcceptUnix()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer c.Close()
|
||||
buf := make([]byte, 1)
|
||||
oob := make([]byte, 128)
|
||||
n, oobn, _, _, err := c.ReadMsgUnix(buf, oob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n < 1 {
|
||||
return nil, errors.New("missing TUN fd marker")
|
||||
}
|
||||
msgs, err := syscall.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range msgs {
|
||||
fds, e := syscall.ParseUnixRights(&m)
|
||||
if e == nil && len(fds) > 0 {
|
||||
return os.NewFile(uintptr(fds[0]), "android-tun"), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("TUN file descriptor was not received")
|
||||
}
|
||||
|
||||
func randomSID() (protocol.VPNSessionID, error) {
|
||||
var sid protocol.VPNSessionID
|
||||
_, err := io.ReadFull(rand.Reader, sid[:])
|
||||
return sid, err
|
||||
}
|
||||
|
||||
type vpnClient struct {
|
||||
tun *os.File
|
||||
sid protocol.VPNSessionID
|
||||
serverAddr string
|
||||
token string
|
||||
ipv4, ipv6 netip.Addr
|
||||
mtu int
|
||||
timeout time.Duration
|
||||
reconnectEvery int
|
||||
upSizer, downSizer *adaptiveSizer
|
||||
control, upload, download *txnLane
|
||||
upPackets, downPackets, upBytes, downBytes atomic.Uint64
|
||||
stopped chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newVPNClient(tun *os.File, addr, token string, v4, v6 netip.Addr, mtu, start, min, max, growAfter, reconnectEvery int, timeout time.Duration, adaptLog bool) (*vpnClient, error) {
|
||||
sid, err := randomSID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vpnClient{tun: tun, sid: sid, serverAddr: addr, token: token, ipv4: v4, ipv6: v6, mtu: mtu, timeout: timeout, reconnectEvery: reconnectEvery,
|
||||
upSizer: newSizer("upload", start, min, max, growAfter, adaptLog), downSizer: newSizer("download", start, min, max, growAfter, adaptLog),
|
||||
control: newTxnLane(addr, timeout, reconnectEvery), upload: newTxnLane(addr, timeout, reconnectEvery), download: newTxnLane(addr, timeout, reconnectEvery), stopped: make(chan struct{})}, nil
|
||||
}
|
||||
func (v *vpnClient) open() error {
|
||||
req, err := protocol.BuildVPNOpen(v.sid, v.token, v.ipv4, v.ipv6, v.mtu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := doControl(v.control, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
max, err := protocol.ParseVPNOpened(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if max < v.upSizer.max {
|
||||
v.upSizer.max = max
|
||||
if v.upSizer.current > max {
|
||||
v.upSizer.current = max
|
||||
}
|
||||
}
|
||||
if max < v.downSizer.max {
|
||||
v.downSizer.max = max
|
||||
if v.downSizer.current > max {
|
||||
v.downSizer.current = max
|
||||
}
|
||||
}
|
||||
fmt.Printf("VPN SESSION OPEN ipv4=%s ipv6=%s mtu=%d server_chunk_max=%d\n", v.ipv4, v.ipv6, v.mtu, max)
|
||||
return nil
|
||||
}
|
||||
func (v *vpnClient) close() {
|
||||
v.stopOnce.Do(func() {
|
||||
close(v.stopped)
|
||||
if p, err := protocol.BuildVPNClose(v.sid), error(nil); err == nil {
|
||||
_, _ = v.control.Do(p)
|
||||
}
|
||||
v.control.Close()
|
||||
v.upload.Close()
|
||||
v.download.Close()
|
||||
_ = v.tun.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func (v *vpnClient) uploadLoop(errs chan<- error) {
|
||||
buf := make([]byte, 65535)
|
||||
var seq uint32
|
||||
for {
|
||||
n, err := v.tun.Read(buf)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if n < 1 {
|
||||
continue
|
||||
}
|
||||
packet := append([]byte(nil), buf[:n]...)
|
||||
if n > 65535 {
|
||||
continue
|
||||
}
|
||||
offset := 0
|
||||
for offset < n {
|
||||
limit := v.upSizer.Current()
|
||||
size := n - offset
|
||||
if size > limit {
|
||||
size = limit
|
||||
}
|
||||
req, e := protocol.BuildVPNPush(v.sid, seq, offset, n, packet[offset:offset+size])
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
resp, e := v.upload.Do(req)
|
||||
if e != nil {
|
||||
v.upSizer.Failure(size)
|
||||
continue
|
||||
}
|
||||
rseq, accepted, e := protocol.ParseVPNAck(resp)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if rseq != seq || accepted < offset || accepted > n {
|
||||
errs <- errors.New("bad server upload ACK")
|
||||
return
|
||||
}
|
||||
fullRecord := size == limit
|
||||
v.upSizer.Success(size, fullRecord)
|
||||
offset = accepted
|
||||
}
|
||||
v.upPackets.Add(1)
|
||||
v.upBytes.Add(uint64(n))
|
||||
seq++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vpnClient) downloadLoop(errs chan<- error) {
|
||||
var want uint32
|
||||
ack := protocol.VPNNoAck
|
||||
offset := 0
|
||||
var packet []byte
|
||||
total := 0
|
||||
for {
|
||||
limit := v.downSizer.Current()
|
||||
req, e := protocol.BuildVPNPull(v.sid, ack, want, offset, limit)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
resp, e := v.download.Do(req)
|
||||
if e != nil {
|
||||
v.downSizer.Failure(limit)
|
||||
continue
|
||||
}
|
||||
seq, roff, rtotal, data, wait, e := protocol.ParseVPNData(resp)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if wait {
|
||||
continue
|
||||
}
|
||||
if seq != want || roff != offset || rtotal < 1 || rtotal > 65535 {
|
||||
errs <- errors.New("bad server download sequence")
|
||||
return
|
||||
}
|
||||
if offset == 0 {
|
||||
total = rtotal
|
||||
packet = make([]byte, 0, total)
|
||||
} else if rtotal != total {
|
||||
errs <- errors.New("download packet size changed")
|
||||
return
|
||||
}
|
||||
packet = append(packet, data...)
|
||||
offset += len(data)
|
||||
v.downSizer.Success(len(data), len(data) == limit)
|
||||
if offset < total {
|
||||
continue
|
||||
}
|
||||
if offset != total {
|
||||
errs <- errors.New("download packet overflow")
|
||||
return
|
||||
}
|
||||
n, e := v.tun.Write(packet)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if n != len(packet) {
|
||||
errs <- io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
v.downPackets.Add(1)
|
||||
v.downBytes.Add(uint64(n))
|
||||
ack = want
|
||||
want++
|
||||
offset = 0
|
||||
packet = nil
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vpnClient) run() error {
|
||||
if err := v.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("VPN READY")
|
||||
errs := make(chan error, 2)
|
||||
go v.uploadLoop(errs)
|
||||
go v.downloadLoop(errs)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case err := <-errs:
|
||||
return err
|
||||
case <-ticker.C:
|
||||
fmt.Printf("STATS up_packets=%d down_packets=%d up_bytes=%d down_bytes=%d upload_chunk=%d download_chunk=%d pollers=1\n", v.upPackets.Load(), v.downPackets.Load(), v.upBytes.Load(), v.downBytes.Load(), v.upSizer.Current(), v.downSizer.Current())
|
||||
case <-v.stopped:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
serverHost := flag.String("server-host", "", "DragonTCP VPN server host/IP")
|
||||
serverPort := flag.Int("server-port", 53, "DragonTCP VPN server TCP port")
|
||||
token := flag.String("token", "change-this-token", "shared token")
|
||||
tunFDSocket := flag.String("tun-fd-socket", "", "Unix socket path used by Android to pass the VpnService TUN fd")
|
||||
tunFD := flag.Int("tun-fd", -1, "existing TUN fd for testing/non-Android use")
|
||||
ipv4Text := flag.String("vpn-ipv4", "10.123.0.2", "client VPN IPv4 address")
|
||||
ipv6Text := flag.String("vpn-ipv6", "fd7a:4472:6167:6f6e::2", "client VPN IPv6 address")
|
||||
mtu := flag.Int("vpn-mtu", 1280, "VPN interface MTU")
|
||||
chunkMax := flag.Int("chunk-max", 65535, "maximum adaptive record bytes")
|
||||
chunkMin := flag.Int("chunk-min", 32, "minimum adaptive record bytes")
|
||||
chunkStart := flag.Int("chunk-start", 65535, "starting record bytes; app sets this equal to max")
|
||||
growAfter := flag.Int("chunk-grow-after", 64, "full successful records before increasing chunk size")
|
||||
timeout := flag.Duration("chunk-timeout", 2*time.Second, "framed transaction timeout")
|
||||
reconnectEvery := flag.Int("chunk-reconnect-every", 32, "reconnect a TCP/53 lane after this many transactions; 0 keeps it open")
|
||||
adaptLog := flag.Bool("chunk-adapt-log", false, "log adaptive chunk changes")
|
||||
flag.Parse()
|
||||
if *serverHost == "" {
|
||||
fmt.Fprintln(os.Stderr, "--server-host is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *serverPort < 1 || *serverPort > 65535 {
|
||||
fmt.Fprintln(os.Stderr, "invalid server port")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkMin < 32 || *chunkMax > protocol.VPNMaxFragment || *chunkMin > *chunkMax {
|
||||
fmt.Fprintf(os.Stderr, "chunks must satisfy 32 <= min <= max <= %d\n", protocol.VPNMaxFragment)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkStart < *chunkMin {
|
||||
*chunkStart = *chunkMin
|
||||
}
|
||||
if *chunkStart > *chunkMax {
|
||||
*chunkStart = *chunkMax
|
||||
}
|
||||
v4, err := netip.ParseAddr(*ipv4Text)
|
||||
if err != nil || !v4.Is4() {
|
||||
fmt.Fprintln(os.Stderr, "invalid --vpn-ipv4")
|
||||
os.Exit(2)
|
||||
}
|
||||
v6, err := netip.ParseAddr(*ipv6Text)
|
||||
if err != nil || !v6.Is6() {
|
||||
fmt.Fprintln(os.Stderr, "invalid --vpn-ipv6")
|
||||
os.Exit(2)
|
||||
}
|
||||
var tun *os.File
|
||||
if *tunFD >= 0 {
|
||||
tun = os.NewFile(uintptr(*tunFD), "tun")
|
||||
} else {
|
||||
if *tunFDSocket == "" {
|
||||
fmt.Fprintln(os.Stderr, "--tun-fd-socket is required on Android")
|
||||
os.Exit(2)
|
||||
}
|
||||
tun, err = receiveTunFD(*tunFDSocket, 10*time.Second)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "receive TUN fd:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
addr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
|
||||
client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *adaptLog)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() { <-sig; client.close() }()
|
||||
if err := client.run(); err != nil && !errors.Is(err, os.ErrClosed) && !errors.Is(err, net.ErrClosed) {
|
||||
fmt.Fprintln(os.Stderr, "VPN stopped:", err)
|
||||
client.close()
|
||||
os.Exit(1)
|
||||
}
|
||||
client.close()
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"dragontcpvpn/internal/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVPNv4Prefix = "10.123.0.0/16"
|
||||
defaultVPNv6Prefix = "fd7a:4472:6167:6f6e::/64"
|
||||
)
|
||||
|
||||
type debugStats struct {
|
||||
enabled bool
|
||||
packets bool
|
||||
started time.Time
|
||||
activeConns atomic.Int64
|
||||
activeSessions atomic.Int64
|
||||
upPackets atomic.Uint64
|
||||
downPackets atomic.Uint64
|
||||
upBytes atomic.Uint64
|
||||
downBytes atomic.Uint64
|
||||
dropped atomic.Uint64
|
||||
errors atomic.Uint64
|
||||
}
|
||||
|
||||
func (d *debugStats) logf(format string, args ...any) {
|
||||
if d != nil && d.enabled {
|
||||
fmt.Printf("[DEBUG] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
func (d *debugStats) packetf(format string, args ...any) {
|
||||
if d != nil && d.packets {
|
||||
fmt.Printf("[PACKET] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
func (d *debugStats) errorf(format string, args ...any) {
|
||||
if d != nil {
|
||||
d.errors.Add(1)
|
||||
if d.enabled {
|
||||
fmt.Printf("[ERROR] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tokenEqual(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
type vpnSession struct {
|
||||
sid protocol.VPNSessionID
|
||||
ipv4 netip.Addr
|
||||
ipv6 netip.Addr
|
||||
mtu int
|
||||
maxChunk int
|
||||
maxPackets int
|
||||
manager *vpnManager
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
packets map[uint32][]byte
|
||||
nextDown uint32
|
||||
closed bool
|
||||
lastSeen time.Time
|
||||
|
||||
upMu sync.Mutex
|
||||
expectedUp uint32
|
||||
currentSeq uint32
|
||||
currentTotal int
|
||||
currentBuf []byte
|
||||
haveCurrent bool
|
||||
lastComplete uint32
|
||||
lastCompleteTotal int
|
||||
haveLastComplete bool
|
||||
}
|
||||
|
||||
func newVPNSession(m *vpnManager, sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu, maxChunk, maxPackets int) *vpnSession {
|
||||
return &vpnSession{
|
||||
sid: sid, ipv4: v4, ipv6: v6, mtu: mtu, maxChunk: maxChunk, maxPackets: maxPackets,
|
||||
manager: m, notify: make(chan struct{}), packets: make(map[uint32][]byte, maxPackets), lastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *vpnSession) signalLocked() {
|
||||
close(s.notify)
|
||||
s.notify = make(chan struct{})
|
||||
}
|
||||
func (s *vpnSession) touchLocked() { s.lastSeen = time.Now() }
|
||||
func (s *vpnSession) touch() { s.mu.Lock(); s.touchLocked(); s.mu.Unlock() }
|
||||
|
||||
func (s *vpnSession) enqueue(packet []byte) bool {
|
||||
if len(packet) == 0 || len(packet) > 65535 {
|
||||
return false
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return false
|
||||
}
|
||||
if len(s.packets) >= s.maxPackets {
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.dropped.Add(1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
seq := s.nextDown
|
||||
s.nextDown++
|
||||
s.packets[seq] = append([]byte(nil), packet...)
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.downPackets.Add(1)
|
||||
s.manager.debug.downBytes.Add(uint64(len(packet)))
|
||||
s.manager.debug.packetf("QUEUE sid=%s seq=%d bytes=%d", shortSID(s.sid), seq, len(packet))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, error) {
|
||||
s.upMu.Lock()
|
||||
defer s.upMu.Unlock()
|
||||
if total < 1 || total > 65535 || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total {
|
||||
return 0, errors.New("invalid packet fragment")
|
||||
}
|
||||
|
||||
if s.haveLastComplete && seq == s.lastComplete {
|
||||
s.touch()
|
||||
return s.lastCompleteTotal, nil
|
||||
}
|
||||
if seq < s.expectedUp {
|
||||
return 0, fmt.Errorf("old upload sequence %d", seq)
|
||||
}
|
||||
if seq > s.expectedUp {
|
||||
return 0, fmt.Errorf("upload sequence %d expected %d", seq, s.expectedUp)
|
||||
}
|
||||
|
||||
if !s.haveCurrent {
|
||||
if offset != 0 {
|
||||
return 0, errors.New("first fragment offset must be zero")
|
||||
}
|
||||
s.haveCurrent = true
|
||||
s.currentSeq = seq
|
||||
s.currentTotal = total
|
||||
s.currentBuf = make([]byte, 0, total)
|
||||
}
|
||||
if s.currentSeq != seq || s.currentTotal != total {
|
||||
return 0, errors.New("packet fragment metadata changed")
|
||||
}
|
||||
|
||||
// Idempotent retry: if this exact offset was already accepted, acknowledge
|
||||
// the existing bytes instead of appending duplicate data.
|
||||
if offset < len(s.currentBuf) {
|
||||
end := offset + len(data)
|
||||
if end <= len(s.currentBuf) && string(s.currentBuf[offset:end]) == string(data) {
|
||||
return len(s.currentBuf), nil
|
||||
}
|
||||
return 0, errors.New("retry fragment does not match accepted data")
|
||||
}
|
||||
if offset != len(s.currentBuf) {
|
||||
return 0, fmt.Errorf("fragment offset %d expected %d", offset, len(s.currentBuf))
|
||||
}
|
||||
|
||||
s.currentBuf = append(s.currentBuf, data...)
|
||||
accepted := len(s.currentBuf)
|
||||
if accepted < total {
|
||||
s.touch()
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
packet := append([]byte(nil), s.currentBuf...)
|
||||
s.haveCurrent = false
|
||||
s.currentBuf = nil
|
||||
|
||||
if err := s.manager.acceptClientPacket(s, packet); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.lastComplete = seq
|
||||
s.lastCompleteTotal = total
|
||||
s.haveLastComplete = true
|
||||
s.expectedUp++
|
||||
s.touch()
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.upPackets.Add(1)
|
||||
s.manager.debug.upBytes.Add(uint64(len(packet)))
|
||||
s.manager.debug.packetf("UP sid=%s seq=%d bytes=%d", shortSID(s.sid), seq, len(packet))
|
||||
}
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (s *vpnSession) pull(ack, want uint32, offset, limit int, wait time.Duration) ([]byte, int, bool, error) {
|
||||
if offset < 0 || limit < 1 || limit > s.maxChunk {
|
||||
return nil, 0, false, errors.New("invalid pull")
|
||||
}
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
if ack != protocol.VPNNoAck {
|
||||
for seq := range s.packets {
|
||||
if seq <= ack {
|
||||
delete(s.packets, seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
if packet, ok := s.packets[want]; ok {
|
||||
if offset >= len(packet) {
|
||||
s.mu.Unlock()
|
||||
return nil, len(packet), false, errors.New("pull offset beyond packet")
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(packet) {
|
||||
end = len(packet)
|
||||
}
|
||||
out := append([]byte(nil), packet[offset:end]...)
|
||||
total := len(packet)
|
||||
s.mu.Unlock()
|
||||
return out, total, false, nil
|
||||
}
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return nil, 0, false, net.ErrClosed
|
||||
}
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-timer.C:
|
||||
return nil, 0, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *vpnSession) close() {
|
||||
s.mu.Lock()
|
||||
if !s.closed {
|
||||
s.closed = true
|
||||
s.signalLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type vpnManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[protocol.VPNSessionID]*vpnSession
|
||||
byIPv4 map[netip.Addr]*vpnSession
|
||||
byIPv6 map[netip.Addr]*vpnSession
|
||||
maxChunk int
|
||||
maxPackets int
|
||||
pollWait time.Duration
|
||||
timeout time.Duration
|
||||
tun *os.File
|
||||
tunWriteMu sync.Mutex
|
||||
mockEcho bool
|
||||
allowPrivate bool
|
||||
debug *debugStats
|
||||
v4Prefix netip.Prefix
|
||||
v6Prefix netip.Prefix
|
||||
}
|
||||
|
||||
func newVPNManager(tun *os.File, mockEcho bool, maxChunk, maxPackets int, pollWait, timeout time.Duration, allowPrivate bool, debug *debugStats) *vpnManager {
|
||||
v4p := netip.MustParsePrefix(defaultVPNv4Prefix)
|
||||
v6p := netip.MustParsePrefix(defaultVPNv6Prefix)
|
||||
m := &vpnManager{
|
||||
sessions: make(map[protocol.VPNSessionID]*vpnSession), byIPv4: make(map[netip.Addr]*vpnSession), byIPv6: make(map[netip.Addr]*vpnSession),
|
||||
maxChunk: maxChunk, maxPackets: maxPackets, pollWait: pollWait, timeout: timeout, tun: tun, mockEcho: mockEcho, allowPrivate: allowPrivate, debug: debug,
|
||||
v4Prefix: v4p, v6Prefix: v6p,
|
||||
}
|
||||
if tun != nil {
|
||||
go m.tunReadLoop()
|
||||
}
|
||||
go m.cleanupLoop()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *vpnManager) addOrGet(sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu int) (*vpnSession, error) {
|
||||
if !m.v4Prefix.Contains(v4) || v4 == netip.MustParseAddr("10.123.0.1") {
|
||||
return nil, errors.New("client IPv4 outside DragonTCP subnet")
|
||||
}
|
||||
if !m.v6Prefix.Contains(v6) || v6 == netip.MustParseAddr("fd7a:4472:6167:6f6e::1") {
|
||||
return nil, errors.New("client IPv6 outside DragonTCP subnet")
|
||||
}
|
||||
if mtu < 576 || mtu > 9000 {
|
||||
return nil, errors.New("invalid client MTU")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if old := m.sessions[sid]; old != nil {
|
||||
if old.ipv4 != v4 || old.ipv6 != v6 {
|
||||
return nil, errors.New("session address mismatch")
|
||||
}
|
||||
old.touch()
|
||||
return old, nil
|
||||
}
|
||||
if m.byIPv4[v4] != nil || m.byIPv6[v6] != nil {
|
||||
return nil, errors.New("client VPN address already in use")
|
||||
}
|
||||
s := newVPNSession(m, sid, v4, v6, mtu, m.maxChunk, m.maxPackets)
|
||||
m.sessions[sid] = s
|
||||
m.byIPv4[v4] = s
|
||||
m.byIPv6[v6] = s
|
||||
if m.debug != nil {
|
||||
m.debug.activeSessions.Add(1)
|
||||
m.debug.logf("SESSION OPEN sid=%s ipv4=%s ipv6=%s mtu=%d", shortSID(sid), v4, v6, mtu)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *vpnManager) get(sid protocol.VPNSessionID) *vpnSession {
|
||||
m.mu.RLock()
|
||||
s := m.sessions[sid]
|
||||
m.mu.RUnlock()
|
||||
return s
|
||||
}
|
||||
func (m *vpnManager) remove(sid protocol.VPNSessionID) {
|
||||
m.mu.Lock()
|
||||
s := m.sessions[sid]
|
||||
if s != nil {
|
||||
delete(m.sessions, sid)
|
||||
delete(m.byIPv4, s.ipv4)
|
||||
delete(m.byIPv6, s.ipv6)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if s != nil {
|
||||
s.close()
|
||||
if m.debug != nil {
|
||||
m.debug.activeSessions.Add(-1)
|
||||
m.debug.logf("SESSION CLOSE sid=%s", shortSID(sid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *vpnManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
cutoff := time.Now().Add(-m.timeout)
|
||||
var stale []protocol.VPNSessionID
|
||||
m.mu.RLock()
|
||||
for sid, s := range m.sessions {
|
||||
s.mu.Lock()
|
||||
last := s.lastSeen
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if closed || last.Before(cutoff) {
|
||||
stale = append(stale, sid)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
for _, sid := range stale {
|
||||
m.remove(sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func packetAddresses(packet []byte) (src, dst netip.Addr, err error) {
|
||||
if len(packet) < 1 {
|
||||
return src, dst, errors.New("empty IP packet")
|
||||
}
|
||||
switch packet[0] >> 4 {
|
||||
case 4:
|
||||
if len(packet) < 20 {
|
||||
return src, dst, errors.New("short IPv4 packet")
|
||||
}
|
||||
total := int(packet[2])<<8 | int(packet[3])
|
||||
if total < 20 || total > len(packet) {
|
||||
return src, dst, errors.New("invalid IPv4 total length")
|
||||
}
|
||||
var a, b [4]byte
|
||||
copy(a[:], packet[12:16])
|
||||
copy(b[:], packet[16:20])
|
||||
return netip.AddrFrom4(a), netip.AddrFrom4(b), nil
|
||||
case 6:
|
||||
if len(packet) < 40 {
|
||||
return src, dst, errors.New("short IPv6 packet")
|
||||
}
|
||||
total := 40 + (int(packet[4])<<8 | int(packet[5]))
|
||||
if total > len(packet) {
|
||||
return src, dst, errors.New("invalid IPv6 payload length")
|
||||
}
|
||||
var a, b [16]byte
|
||||
copy(a[:], packet[8:24])
|
||||
copy(b[:], packet[24:40])
|
||||
return netip.AddrFrom16(a), netip.AddrFrom16(b), nil
|
||||
default:
|
||||
return src, dst, errors.New("unsupported IP version")
|
||||
}
|
||||
}
|
||||
|
||||
func destinationAllowed(dst netip.Addr, allowPrivate bool) bool {
|
||||
if dst.IsUnspecified() || dst.IsMulticast() {
|
||||
return false
|
||||
}
|
||||
if allowPrivate {
|
||||
return true
|
||||
}
|
||||
if dst.IsLoopback() || dst.IsLinkLocalUnicast() || dst.IsPrivate() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) error {
|
||||
src, dst, err := packetAddresses(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if src != s.ipv4 && src != s.ipv6 {
|
||||
return fmt.Errorf("source %s does not match session address", src)
|
||||
}
|
||||
if !destinationAllowed(dst, m.allowPrivate) {
|
||||
return fmt.Errorf("destination %s is blocked; use --allow-private to permit it", dst)
|
||||
}
|
||||
if m.mockEcho {
|
||||
s.enqueue(packet)
|
||||
return nil
|
||||
}
|
||||
if m.tun == nil {
|
||||
return errors.New("VPN TUN is unavailable")
|
||||
}
|
||||
m.tunWriteMu.Lock()
|
||||
n, err := m.tun.Write(packet)
|
||||
m.tunWriteMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(packet) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *vpnManager) tunReadLoop() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, err := m.tun.Read(buf)
|
||||
if err != nil {
|
||||
if m.debug != nil {
|
||||
m.debug.errorf("TUN read: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n < 1 {
|
||||
continue
|
||||
}
|
||||
packet := append([]byte(nil), buf[:n]...)
|
||||
_, dst, e := packetAddresses(packet)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
m.mu.RLock()
|
||||
var s *vpnSession
|
||||
if dst.Is4() {
|
||||
s = m.byIPv4[dst]
|
||||
} else {
|
||||
s = m.byIPv6[dst]
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
if s != nil {
|
||||
s.enqueue(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shortSID(sid protocol.VPNSessionID) string { return hex.EncodeToString(sid[:4]) }
|
||||
|
||||
func processVPN(conn net.Conn, requestID uint32, payload []byte, token string, m *vpnManager) error {
|
||||
switch payload[0] {
|
||||
case protocol.VPNCmdOpen:
|
||||
sid, tok, v4, v6, mtu, err := protocol.ParseVPNOpen(payload)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
if !tokenEqual(tok, token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("authentication failed"))
|
||||
}
|
||||
_, err = m.addOrGet(sid, v4, v6, mtu)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNOpened(m.maxChunk))
|
||||
case protocol.VPNCmdPush:
|
||||
sid, seq, offset, total, data, err := protocol.ParseVPNPush(payload)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
s := m.get(sid)
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN session"))
|
||||
}
|
||||
accepted, err := s.push(seq, offset, total, data)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNAck(seq, accepted))
|
||||
case protocol.VPNCmdPull:
|
||||
sid, ack, want, offset, limit, err := protocol.ParseVPNPull(payload)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
s := m.get(sid)
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN session"))
|
||||
}
|
||||
if limit > s.maxChunk {
|
||||
limit = s.maxChunk
|
||||
}
|
||||
data, total, wait, err := s.pull(ack, want, offset, limit, m.pollWait)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
if wait {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte{protocol.VPNRespWait})
|
||||
}
|
||||
m.debug.packetf("DOWN sid=%s seq=%d offset=%d bytes=%d total=%d", shortSID(sid), want, offset, len(data), total)
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNData(want, offset, total, data))
|
||||
case protocol.VPNCmdClose:
|
||||
sid, err := protocol.ParseVPNClose(payload)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
||||
}
|
||||
m.remove(sid)
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte{protocol.VPNRespClosed})
|
||||
default:
|
||||
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN command"))
|
||||
}
|
||||
}
|
||||
|
||||
func handleConn(conn net.Conn, token string, m *vpnManager, slots chan struct{}, debug *debugStats) {
|
||||
defer func() { <-slots; debug.activeConns.Add(-1); _ = conn.Close() }()
|
||||
protocol.TuneTCP(conn)
|
||||
for {
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
||||
debug.errorf("peer=%v read: %v", conn.RemoteAddr(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !protocol.IsVPNCommand(payload) {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("this binary accepts DragonTCP VPN packet commands only"))
|
||||
continue
|
||||
}
|
||||
if err := processVPN(conn, requestID, payload, token, m); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Linux TUN setup.
|
||||
type ifreq struct {
|
||||
Name [16]byte
|
||||
Flags uint16
|
||||
_ [22]byte
|
||||
}
|
||||
|
||||
const tunSetIFF = 0x400454ca
|
||||
const iffTun = 0x0001
|
||||
const iffNoPI = 0x1000
|
||||
|
||||
func openTun(name string) (*os.File, error) {
|
||||
fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|syscall.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var req ifreq
|
||||
copy(req.Name[:], []byte(name))
|
||||
req.Flags = iffTun | iffNoPI
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tunSetIFF), uintptr(unsafe.Pointer(&req)))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return nil, errno
|
||||
}
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
|
||||
func run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
out, err := c.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %v: %s", cmd, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func runOptional(debug *debugStats, cmd string, args ...string) {
|
||||
if err := run(cmd, args...); err != nil {
|
||||
debug.logf("optional command failed: %v", err)
|
||||
}
|
||||
}
|
||||
func ensureRule(debug *debugStats, binary string, argsCheck, argsAdd []string) {
|
||||
if err := exec.Command(binary, argsCheck...).Run(); err == nil {
|
||||
return
|
||||
}
|
||||
if err := run(binary, argsAdd...); err != nil {
|
||||
debug.logf("NAT rule warning: %v", err)
|
||||
}
|
||||
}
|
||||
func setupLinuxVPN(tunName string, mtu int, autoNAT bool, debug *debugStats) (*os.File, error) {
|
||||
tun, err := openTun(tunName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open /dev/net/tun: %w", err)
|
||||
}
|
||||
fail := func(e error) (*os.File, error) { tun.Close(); return nil, e }
|
||||
if err := run("ip", "link", "set", "dev", tunName, "mtu", strconv.Itoa(mtu)); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := run("ip", "addr", "replace", "10.123.0.1/16", "dev", tunName); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
// IPv6 may be disabled on some hosts; report clearly instead of silently bypassing it.
|
||||
if err := run("ip", "-6", "addr", "replace", "fd7a:4472:6167:6f6e::1/64", "dev", tunName); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := run("ip", "link", "set", "dev", tunName, "up"); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
|
||||
return fail(fmt.Errorf("enable IPv4 forwarding: %w", err))
|
||||
}
|
||||
if err := os.WriteFile("/proc/sys/net/ipv6/conf/all/forwarding", []byte("1\n"), 0644); err != nil {
|
||||
return fail(fmt.Errorf("enable IPv6 forwarding: %w", err))
|
||||
}
|
||||
if autoNAT {
|
||||
if _, err := exec.LookPath("iptables"); err != nil {
|
||||
return fail(errors.New("iptables not found; install iptables or start with --auto-nat=false and configure NAT yourself"))
|
||||
}
|
||||
ensureRule(debug, "iptables", []string{"-t", "nat", "-C", "POSTROUTING", "-s", "10.123.0.0/16", "-j", "MASQUERADE"}, []string{"-t", "nat", "-A", "POSTROUTING", "-s", "10.123.0.0/16", "-j", "MASQUERADE"})
|
||||
ensureRule(debug, "iptables", []string{"-C", "FORWARD", "-i", tunName, "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-i", tunName, "-j", "ACCEPT"})
|
||||
ensureRule(debug, "iptables", []string{"-C", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
|
||||
if _, err := exec.LookPath("ip6tables"); err == nil {
|
||||
ensureRule(debug, "ip6tables", []string{"-t", "nat", "-C", "POSTROUTING", "-s", "fd7a:4472:6167:6f6e::/64", "-j", "MASQUERADE"}, []string{"-t", "nat", "-A", "POSTROUTING", "-s", "fd7a:4472:6167:6f6e::/64", "-j", "MASQUERADE"})
|
||||
ensureRule(debug, "ip6tables", []string{"-C", "FORWARD", "-i", tunName, "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-i", tunName, "-j", "ACCEPT"})
|
||||
ensureRule(debug, "ip6tables", []string{"-C", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
|
||||
} else {
|
||||
debug.logf("WARNING: ip6tables not found; IPv6 Internet access needs manual routing/NAT")
|
||||
}
|
||||
}
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
host := flag.String("host", "0.0.0.0", "listen host")
|
||||
port := flag.Int("port", 53, "listen TCP port")
|
||||
token := flag.String("token", "change-this-token", "shared token")
|
||||
maxConnections := flag.Int("max-connections", 20000, "maximum simultaneous TCP/53 connections")
|
||||
maxChunk := flag.Int("chunk-max", 65535, "maximum VPN fragment payload bytes (32-65535)")
|
||||
maxPackets := flag.Int("vpn-buffered-packets", 2048, "maximum queued return IP packets per client")
|
||||
pollWait := flag.Duration("poll-wait", 100*time.Millisecond, "long-poll wait for a return packet")
|
||||
sessionTimeout := flag.Duration("session-timeout", 5*time.Minute, "idle VPN session timeout")
|
||||
tunName := flag.String("tun", "dragontcp0", "Linux TUN interface name")
|
||||
mtu := flag.Int("mtu", 1280, "server TUN MTU")
|
||||
autoNAT := flag.Bool("auto-nat", true, "configure IPv4/IPv6 forwarding and iptables MASQUERADE")
|
||||
allowPrivate := flag.Bool("allow-private", false, "allow VPN clients to access private/link-local destinations")
|
||||
mockEcho := flag.Bool("mock-echo", false, "test mode: echo client IP packets back instead of using Linux TUN/NAT")
|
||||
debugOn := flag.Bool("debug", false, "debug sessions and statistics")
|
||||
debugPackets := flag.Bool("debug-packets", false, "very verbose per-IP-packet logging")
|
||||
statsEvery := flag.Duration("debug-stats-interval", 10*time.Second, "debug statistics interval; 0 disables")
|
||||
flag.Parse()
|
||||
if *maxChunk < 32 || *maxChunk > protocol.VPNMaxFragment {
|
||||
fmt.Fprintf(os.Stderr, "--chunk-max must be 32-%d\n", protocol.VPNMaxFragment)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *mtu < 576 || *mtu > 9000 {
|
||||
fmt.Fprintln(os.Stderr, "--mtu must be 576-9000")
|
||||
os.Exit(2)
|
||||
}
|
||||
debug := &debugStats{enabled: *debugOn, packets: *debugPackets, started: time.Now()}
|
||||
var tun *os.File
|
||||
var err error
|
||||
if !*mockEcho {
|
||||
tun, err = setupLinuxVPN(*tunName, *mtu, *autoNAT, debug)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "VPN setup failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer tun.Close()
|
||||
}
|
||||
manager := newVPNManager(tun, *mockEcho, *maxChunk, *maxPackets, *pollWait, *sessionTimeout, *allowPrivate, debug)
|
||||
addr := net.JoinHostPort(*host, strconv.Itoa(*port))
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer ln.Close()
|
||||
fmt.Printf("DragonTCP VPN server listening on %s\n", addr)
|
||||
if *mockEcho {
|
||||
fmt.Println("mode=mock-echo (no Internet forwarding)")
|
||||
} else {
|
||||
fmt.Printf("tun=%s mtu=%d IPv4=10.123.0.1/16 IPv6=fd7a:4472:6167:6f6e::1/64 auto_nat=%t\n", *tunName, *mtu, *autoNAT)
|
||||
}
|
||||
fmt.Printf("chunk_max=%d poll_wait=%s buffered_packets=%d\n", *maxChunk, pollWait.String(), *maxPackets)
|
||||
if debug.enabled && *statsEvery > 0 {
|
||||
go func() {
|
||||
t := time.NewTicker(*statsEvery)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
fmt.Printf("[DEBUG] STATS uptime=%s conns=%d sessions=%d up_packets=%d down_packets=%d up_bytes=%d down_bytes=%d dropped=%d errors=%d\n", time.Since(debug.started).Round(time.Second), debug.activeConns.Load(), debug.activeSessions.Load(), debug.upPackets.Load(), debug.downPackets.Load(), debug.upBytes.Load(), debug.downBytes.Load(), debug.dropped.Load(), debug.errors.Load())
|
||||
}
|
||||
}()
|
||||
}
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() { <-sig; fmt.Println("Stopping DragonTCP VPN server..."); ln.Close() }()
|
||||
slots := make(chan struct{}, *maxConnections)
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
debug.activeConns.Add(1)
|
||||
go handleConn(conn, *token, manager, slots, debug)
|
||||
default:
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module dragontcpvpn
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,267 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
const (
|
||||
VPNCmdOpen byte = 0x30
|
||||
VPNCmdPush byte = 0x31
|
||||
VPNCmdPull byte = 0x32
|
||||
VPNCmdClose byte = 0x33
|
||||
|
||||
VPNRespOpened byte = 0x40
|
||||
VPNRespAck byte = 0x41
|
||||
VPNRespData byte = 0x42
|
||||
VPNRespWait byte = 0x43
|
||||
VPNRespClosed byte = 0x44
|
||||
VPNRespError byte = 0x7f
|
||||
|
||||
VPNNoAck uint32 = 0xffffffff
|
||||
VPNMaxFragment = 65535
|
||||
)
|
||||
|
||||
type VPNSessionID [16]byte
|
||||
|
||||
func VPNError(message string) []byte {
|
||||
b := []byte(message)
|
||||
if len(b) > 4096 {
|
||||
b = b[:4096]
|
||||
}
|
||||
out := make([]byte, 1+len(b))
|
||||
out[0] = VPNRespError
|
||||
copy(out[1:], b)
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNError(payload []byte) error {
|
||||
if len(payload) == 0 {
|
||||
return errors.New("empty DragonTCP VPN response")
|
||||
}
|
||||
if payload[0] == VPNRespError {
|
||||
return errors.New(string(payload[1:]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OPEN request:
|
||||
// cmd(1) sid(16) tokenLen(2) token(N) ipv4(4) ipv6(16) mtu(2)
|
||||
func BuildVPNOpen(sid VPNSessionID, token string, ipv4, ipv6 netip.Addr, mtu int) ([]byte, error) {
|
||||
if len(token) > 4096 {
|
||||
return nil, errors.New("token too long")
|
||||
}
|
||||
if !ipv4.Is4() || !ipv6.Is6() {
|
||||
return nil, errors.New("invalid VPN client addresses")
|
||||
}
|
||||
if mtu < 576 || mtu > 65535 {
|
||||
return nil, errors.New("invalid VPN MTU")
|
||||
}
|
||||
out := make([]byte, 1+16+2+len(token)+4+16+2)
|
||||
out[0] = VPNCmdOpen
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint16(out[17:19], uint16(len(token)))
|
||||
pos := 19
|
||||
copy(out[pos:pos+len(token)], token)
|
||||
pos += len(token)
|
||||
v4 := ipv4.As4()
|
||||
copy(out[pos:pos+4], v4[:])
|
||||
pos += 4
|
||||
v6 := ipv6.As16()
|
||||
copy(out[pos:pos+16], v6[:])
|
||||
pos += 16
|
||||
binary.BigEndian.PutUint16(out[pos:pos+2], uint16(mtu))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNOpen(payload []byte) (sid VPNSessionID, token string, ipv4, ipv6 netip.Addr, mtu int, err error) {
|
||||
if len(payload) < 1+16+2+4+16+2 || payload[0] != VPNCmdOpen {
|
||||
err = errors.New("bad VPN OPEN")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
tokenLen := int(binary.BigEndian.Uint16(payload[17:19]))
|
||||
need := 1 + 16 + 2 + tokenLen + 4 + 16 + 2
|
||||
if tokenLen < 0 || len(payload) != need {
|
||||
err = errors.New("bad VPN OPEN length")
|
||||
return
|
||||
}
|
||||
pos := 19
|
||||
token = string(payload[pos : pos+tokenLen])
|
||||
pos += tokenLen
|
||||
var a4 [4]byte
|
||||
copy(a4[:], payload[pos:pos+4])
|
||||
ipv4 = netip.AddrFrom4(a4)
|
||||
pos += 4
|
||||
var a6 [16]byte
|
||||
copy(a6[:], payload[pos:pos+16])
|
||||
ipv6 = netip.AddrFrom16(a6)
|
||||
pos += 16
|
||||
mtu = int(binary.BigEndian.Uint16(payload[pos : pos+2]))
|
||||
return
|
||||
}
|
||||
|
||||
func BuildVPNOpened(maxChunk int) []byte {
|
||||
if maxChunk > VPNMaxFragment {
|
||||
maxChunk = VPNMaxFragment
|
||||
}
|
||||
if maxChunk < 1 {
|
||||
maxChunk = 1
|
||||
}
|
||||
out := make([]byte, 3)
|
||||
out[0] = VPNRespOpened
|
||||
binary.BigEndian.PutUint16(out[1:3], uint16(maxChunk))
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNOpened(payload []byte) (int, error) {
|
||||
if err := ParseVPNError(payload); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(payload) != 3 || payload[0] != VPNRespOpened {
|
||||
return 0, errors.New("bad VPN OPENED response")
|
||||
}
|
||||
return int(binary.BigEndian.Uint16(payload[1:3])), nil
|
||||
}
|
||||
|
||||
// PUSH request: cmd(1) sid(16) seq(4) offset(2) total(2) data(N)
|
||||
func BuildVPNPush(sid VPNSessionID, seq uint32, offset, total int, data []byte) ([]byte, error) {
|
||||
if total < 1 || total > 65535 || offset < 0 || offset > total || len(data) < 1 || offset+len(data) > total || len(data) > VPNMaxFragment {
|
||||
return nil, errors.New("invalid VPN PUSH fragment")
|
||||
}
|
||||
out := make([]byte, 25+len(data))
|
||||
out[0] = VPNCmdPush
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint32(out[17:21], seq)
|
||||
binary.BigEndian.PutUint16(out[21:23], uint16(offset))
|
||||
binary.BigEndian.PutUint16(out[23:25], uint16(total))
|
||||
copy(out[25:], data)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNPush(payload []byte) (sid VPNSessionID, seq uint32, offset, total int, data []byte, err error) {
|
||||
if len(payload) < 26 || payload[0] != VPNCmdPush {
|
||||
err = errors.New("bad VPN PUSH")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
seq = binary.BigEndian.Uint32(payload[17:21])
|
||||
offset = int(binary.BigEndian.Uint16(payload[21:23]))
|
||||
total = int(binary.BigEndian.Uint16(payload[23:25]))
|
||||
data = payload[25:]
|
||||
if total < 1 || offset < 0 || offset > total || len(data) < 1 || offset+len(data) > total {
|
||||
err = errors.New("bad VPN PUSH fragment bounds")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BuildVPNAck(seq uint32, accepted int) []byte {
|
||||
out := make([]byte, 7)
|
||||
out[0] = VPNRespAck
|
||||
binary.BigEndian.PutUint32(out[1:5], seq)
|
||||
binary.BigEndian.PutUint16(out[5:7], uint16(accepted))
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNAck(payload []byte) (seq uint32, accepted int, err error) {
|
||||
if e := ParseVPNError(payload); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
if len(payload) != 7 || payload[0] != VPNRespAck {
|
||||
err = errors.New("bad VPN ACK")
|
||||
return
|
||||
}
|
||||
seq = binary.BigEndian.Uint32(payload[1:5])
|
||||
accepted = int(binary.BigEndian.Uint16(payload[5:7]))
|
||||
return
|
||||
}
|
||||
|
||||
// PULL request: cmd(1) sid(16) ack(4) want(4) offset(2) limit(2)
|
||||
func BuildVPNPull(sid VPNSessionID, ack, want uint32, offset, limit int) ([]byte, error) {
|
||||
if offset < 0 || offset > 65535 || limit < 1 || limit > VPNMaxFragment {
|
||||
return nil, errors.New("invalid VPN PULL")
|
||||
}
|
||||
out := make([]byte, 29)
|
||||
out[0] = VPNCmdPull
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint32(out[17:21], ack)
|
||||
binary.BigEndian.PutUint32(out[21:25], want)
|
||||
binary.BigEndian.PutUint16(out[25:27], uint16(offset))
|
||||
binary.BigEndian.PutUint16(out[27:29], uint16(limit))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNPull(payload []byte) (sid VPNSessionID, ack, want uint32, offset, limit int, err error) {
|
||||
if len(payload) != 29 || payload[0] != VPNCmdPull {
|
||||
err = errors.New("bad VPN PULL")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
ack = binary.BigEndian.Uint32(payload[17:21])
|
||||
want = binary.BigEndian.Uint32(payload[21:25])
|
||||
offset = int(binary.BigEndian.Uint16(payload[25:27]))
|
||||
limit = int(binary.BigEndian.Uint16(payload[27:29]))
|
||||
if limit < 1 {
|
||||
err = errors.New("bad VPN PULL limit")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DATA response: cmd(1) seq(4) offset(2) total(2) data(N)
|
||||
func BuildVPNData(seq uint32, offset, total int, data []byte) []byte {
|
||||
out := make([]byte, 9+len(data))
|
||||
out[0] = VPNRespData
|
||||
binary.BigEndian.PutUint32(out[1:5], seq)
|
||||
binary.BigEndian.PutUint16(out[5:7], uint16(offset))
|
||||
binary.BigEndian.PutUint16(out[7:9], uint16(total))
|
||||
copy(out[9:], data)
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNData(payload []byte) (seq uint32, offset, total int, data []byte, wait bool, err error) {
|
||||
if e := ParseVPNError(payload); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
if len(payload) == 1 && payload[0] == VPNRespWait {
|
||||
wait = true
|
||||
return
|
||||
}
|
||||
if len(payload) < 10 || payload[0] != VPNRespData {
|
||||
err = fmt.Errorf("bad VPN DATA response type/length")
|
||||
return
|
||||
}
|
||||
seq = binary.BigEndian.Uint32(payload[1:5])
|
||||
offset = int(binary.BigEndian.Uint16(payload[5:7]))
|
||||
total = int(binary.BigEndian.Uint16(payload[7:9]))
|
||||
data = payload[9:]
|
||||
if total < 1 || offset < 0 || offset+len(data) > total || len(data) < 1 {
|
||||
err = errors.New("bad VPN DATA bounds")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BuildVPNClose(sid VPNSessionID) []byte {
|
||||
out := make([]byte, 17)
|
||||
out[0] = VPNCmdClose
|
||||
copy(out[1:17], sid[:])
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNClose(payload []byte) (sid VPNSessionID, err error) {
|
||||
if len(payload) != 17 || payload[0] != VPNCmdClose {
|
||||
return sid, errors.New("bad VPN CLOSE")
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
func IsVPNCommand(payload []byte) bool {
|
||||
if len(payload) == 0 {
|
||||
return false
|
||||
}
|
||||
return payload[0] >= VPNCmdOpen && payload[0] <= VPNCmdClose
|
||||
}
|
||||
@@ -1,748 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
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, opts chunkClientOptions) *adaptiveSizer {
|
||||
start := opts.startSize
|
||||
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: opts.adaptSuccesses,
|
||||
logChanges: opts.adaptLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Current() int {
|
||||
s.mu.Lock()
|
||||
n := s.current
|
||||
s.mu.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Success(attempted int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.adaptive || s.current >= s.max {
|
||||
return
|
||||
}
|
||||
// Ignore stale successes from records that were already in flight when
|
||||
// another worker changed the shared size.
|
||||
if attempted != s.current {
|
||||
return
|
||||
}
|
||||
|
||||
if attempted > s.good {
|
||||
s.good = attempted
|
||||
}
|
||||
s.successes++
|
||||
|
||||
growAfter := s.adaptSuccesses
|
||||
// When we have converged close to a known failure boundary, stay stable
|
||||
// longer before probing again. This also lets us discover later network
|
||||
// improvements without constantly oscillating around the boundary.
|
||||
if s.bad > 0 && s.bad-s.good <= 32 {
|
||||
growAfter *= 8
|
||||
}
|
||||
if s.successes < growAfter {
|
||||
return
|
||||
}
|
||||
s.successes = 0
|
||||
|
||||
old := s.current
|
||||
var next int
|
||||
if s.bad > old+1 {
|
||||
// Binary-search the gap between known-good and known-bad sizes.
|
||||
next = old + (s.bad-old)/2
|
||||
} else {
|
||||
// Either there is no known ceiling, or we have stayed stable long enough
|
||||
// at it to probe the network again in case conditions improved.
|
||||
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) (old, next int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
old = s.current
|
||||
|
||||
if !s.adaptive {
|
||||
return old, old
|
||||
}
|
||||
// Multiple pollers can fail on the same oversized value at once. Only the
|
||||
// first failure for the current value is allowed to reduce it.
|
||||
if attempted != s.current {
|
||||
return old, old
|
||||
}
|
||||
s.successes = 0
|
||||
|
||||
if s.bad == 0 || attempted < s.bad {
|
||||
s.bad = attempted
|
||||
}
|
||||
|
||||
if s.good > 0 && s.good < attempted {
|
||||
// Return directly to the last size that was proven to work.
|
||||
next = s.good
|
||||
} else {
|
||||
// A previously-good value just failed, so conditions worsened. Forget
|
||||
// the old lower bound and use multiplicative decrease.
|
||||
s.good = 0
|
||||
next = attempted / 2
|
||||
}
|
||||
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 && next != old {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
}
|
||||
return old, next
|
||||
}
|
||||
|
||||
type txnLane struct {
|
||||
mu sync.Mutex
|
||||
serverAddr string
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
conn net.Conn
|
||||
count int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *txnLane {
|
||||
return &txnLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *txnLane) closeLocked() {
|
||||
if l.conn != nil {
|
||||
_ = l.conn.Close()
|
||||
l.conn = nil
|
||||
}
|
||||
l.count = 0
|
||||
}
|
||||
|
||||
func (l *txnLane) Close() {
|
||||
l.mu.Lock()
|
||||
l.closed = true
|
||||
l.closeLocked()
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *txnLane) ensureConn() error {
|
||||
if l.closed {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if l.conn != nil && (l.reconnectEvery <= 0 || l.count < l.reconnectEvery) {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.closeLocked()
|
||||
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.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do performs exactly one framed transaction. Higher layers decide whether a
|
||||
// failed data record should be retried at a smaller adaptive size.
|
||||
func (l *txnLane) Do(payload []byte) ([]byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if err := l.ensureConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.conn.SetDeadline(time.Now().Add(timeout))
|
||||
requestID := requestCounter.Add(1)
|
||||
|
||||
if err := protocol.WriteRequestFrame(l.conn, requestID, payload); err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseID, response, err := protocol.ReadResponseFrame(l.conn)
|
||||
if err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
if responseID != requestID {
|
||||
l.closeLocked()
|
||||
return nil, fmt.Errorf("request ID mismatch")
|
||||
}
|
||||
|
||||
l.count++
|
||||
_ = l.conn.SetDeadline(time.Time{})
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func doControl(lane *txnLane, payload []byte) ([]byte, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
resp, err := lane.Do(payload)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(time.Duration(attempt+1) * 40 * time.Millisecond)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
type chunkResult struct {
|
||||
seq uint64
|
||||
data []byte
|
||||
final uint64
|
||||
eof bool
|
||||
err error
|
||||
}
|
||||
|
||||
type chunkConn struct {
|
||||
serverAddr string
|
||||
token string
|
||||
sid string
|
||||
opts chunkClientOptions
|
||||
|
||||
pushLane *txnLane
|
||||
pullLanes []*txnLane
|
||||
|
||||
upSizer *adaptiveSizer
|
||||
downSizer *adaptiveSizer
|
||||
serverMax int
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
once sync.Once
|
||||
|
||||
writeMu sync.Mutex
|
||||
upSeq uint64
|
||||
|
||||
claim atomic.Uint64
|
||||
ack atomic.Int64
|
||||
|
||||
results chan chunkResult
|
||||
workers sync.WaitGroup
|
||||
|
||||
readMu sync.Mutex
|
||||
pending map[uint64][]byte
|
||||
nextRead uint64
|
||||
current []byte
|
||||
currentSeq uint64
|
||||
finalKnown bool
|
||||
finalSeq uint64
|
||||
terminalErr error
|
||||
}
|
||||
|
||||
func randomSessionID() (string, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b[:]), nil
|
||||
}
|
||||
|
||||
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 > protocol.MaxChunkPayload {
|
||||
opts.maxSize = protocol.MaxChunkPayload
|
||||
}
|
||||
if opts.startSize < opts.minSize {
|
||||
opts.startSize = opts.minSize
|
||||
}
|
||||
if opts.startSize > opts.maxSize {
|
||||
opts.startSize = opts.maxSize
|
||||
}
|
||||
if opts.adaptSuccesses < 1 {
|
||||
opts.adaptSuccesses = 64
|
||||
}
|
||||
if opts.pollers < 1 {
|
||||
opts.pollers = 1
|
||||
}
|
||||
if opts.pollers > 128 {
|
||||
opts.pollers = 128
|
||||
}
|
||||
if opts.txnTimeout <= 0 {
|
||||
opts.txnTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &chunkConn{
|
||||
serverAddr: serverAddr,
|
||||
token: token,
|
||||
sid: sid,
|
||||
opts: opts,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
results: make(chan chunkResult, opts.pollers*4),
|
||||
pending: make(map[uint64][]byte, opts.pollers*2),
|
||||
}
|
||||
c.ack.Store(-1)
|
||||
c.upSizer = newAdaptiveSizer("upload", opts)
|
||||
c.downSizer = newAdaptiveSizer("download", opts)
|
||||
|
||||
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
|
||||
openPayload := []byte(fmt.Sprintf(
|
||||
"COPEN %s %s %s %d",
|
||||
token, sid, targetHost, targetPort,
|
||||
))
|
||||
resp, err := doControl(c.pushLane, openPayload)
|
||||
if err != nil {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
fields := strings.Fields(string(resp))
|
||||
if len(fields) != 2 || fields[0] != "OPENED" {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, fmt.Errorf("%s", resp)
|
||||
}
|
||||
serverMax, err := strconv.Atoi(fields[1])
|
||||
if err != nil || serverMax < 32 {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, fmt.Errorf("bad OPENED response: %q", resp)
|
||||
}
|
||||
c.serverMax = serverMax
|
||||
if serverMax < c.opts.maxSize {
|
||||
c.opts.maxSize = serverMax
|
||||
c.upSizer.max = serverMax
|
||||
c.downSizer.max = serverMax
|
||||
if c.upSizer.current > serverMax {
|
||||
c.upSizer.current = serverMax
|
||||
}
|
||||
if c.downSizer.current > serverMax {
|
||||
c.downSizer.current = serverMax
|
||||
}
|
||||
}
|
||||
|
||||
c.pullLanes = make([]*txnLane, opts.pollers)
|
||||
for i := 0; i < opts.pollers; i++ {
|
||||
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
c.pullLanes[i] = lane
|
||||
c.workers.Add(1)
|
||||
go c.pullWorker(lane)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseDataResponse(resp []byte) (seq uint64, offset int, total int, data []byte, err error) {
|
||||
if len(resp) < 6 || string(resp[:5]) != "DATA " {
|
||||
return 0, 0, 0, nil, fmt.Errorf("not DATA")
|
||||
}
|
||||
|
||||
rest := resp[5:]
|
||||
fields := make([][]byte, 0, 3)
|
||||
start := 0
|
||||
for i := 0; i < len(rest) && len(fields) < 3; i++ {
|
||||
if rest[i] == ' ' {
|
||||
fields = append(fields, rest[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if len(fields) != 3 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA response")
|
||||
}
|
||||
|
||||
seq, err = strconv.ParseUint(string(fields[0]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
offset, err = strconv.Atoi(string(fields[1]))
|
||||
if err != nil || offset < 0 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA offset")
|
||||
}
|
||||
total, err = strconv.Atoi(string(fields[2]))
|
||||
if err != nil || total < 0 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA total")
|
||||
}
|
||||
|
||||
// start now points immediately after the third separator.
|
||||
return seq, offset, total, rest[start:], nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) pullWorker(lane *txnLane) {
|
||||
defer c.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
seq := c.claim.Add(1) - 1
|
||||
offset := 0
|
||||
var assembled []byte
|
||||
consecutiveMinFailures := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
limit := c.downSizer.Current()
|
||||
ack := c.ack.Load()
|
||||
payload := []byte(fmt.Sprintf(
|
||||
"CPULL %s %s %d %d %d %d",
|
||||
c.token, c.sid, ack, seq, offset, limit,
|
||||
))
|
||||
|
||||
resp, err := lane.Do(payload)
|
||||
if err != nil {
|
||||
old, next := c.downSizer.Failure(limit)
|
||||
if next == old && next == c.opts.minSize {
|
||||
consecutiveMinFailures++
|
||||
} else {
|
||||
consecutiveMinFailures = 0
|
||||
}
|
||||
if consecutiveMinFailures >= 8 {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("download failed at minimum chunk %d: %w", next, err)}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
if string(resp) == "WAIT" {
|
||||
if c.opts.pollDelay > 0 {
|
||||
select {
|
||||
case <-time.After(c.opts.pollDelay):
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "ERR ") {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("%s", resp)}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "EOF ") {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(string(resp[4:])), 10, 64)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: err}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, eof: true, final: n}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
gotSeq, gotOffset, total, fragment, err := parseDataResponse(resp)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: err}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotSeq != seq || gotOffset != offset {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("DATA position mismatch")}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
if total > c.serverMax || total < offset+len(fragment) || len(fragment) == 0 {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("invalid DATA fragment size")}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if assembled == nil {
|
||||
assembled = make([]byte, 0, total)
|
||||
}
|
||||
assembled = append(assembled, fragment...)
|
||||
offset += len(fragment)
|
||||
consecutiveMinFailures = 0
|
||||
c.downSizer.Success(limit)
|
||||
|
||||
if offset == total {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, data: assembled}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chunkConn) Read(p []byte) (int, error) {
|
||||
c.readMu.Lock()
|
||||
defer c.readMu.Unlock()
|
||||
|
||||
for {
|
||||
if len(c.current) > 0 {
|
||||
n := copy(p, c.current)
|
||||
c.current = c.current[n:]
|
||||
if len(c.current) == 0 {
|
||||
c.nextRead++
|
||||
c.ack.Store(int64(c.currentSeq))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if c.terminalErr != nil {
|
||||
return 0, c.terminalErr
|
||||
}
|
||||
|
||||
if c.finalKnown && c.nextRead >= c.finalSeq {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if data, ok := c.pending[c.nextRead]; ok {
|
||||
delete(c.pending, c.nextRead)
|
||||
c.current = data
|
||||
c.currentSeq = c.nextRead
|
||||
continue
|
||||
}
|
||||
|
||||
result, ok := <-c.results
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if result.err != nil {
|
||||
c.terminalErr = result.err
|
||||
return 0, result.err
|
||||
}
|
||||
if result.eof {
|
||||
if !c.finalKnown || result.final < c.finalSeq {
|
||||
c.finalKnown = true
|
||||
c.finalSeq = result.final
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.seq < c.nextRead {
|
||||
continue
|
||||
}
|
||||
c.pending[result.seq] = result.data
|
||||
}
|
||||
}
|
||||
|
||||
func parseAck(resp []byte, expectedSeq uint64) (int, error) {
|
||||
fields := strings.Fields(string(resp))
|
||||
if len(fields) != 3 || fields[0] != "ACK" {
|
||||
return 0, fmt.Errorf("bad CPUSH response: %q", resp)
|
||||
}
|
||||
seq, err := strconv.ParseUint(fields[1], 10, 64)
|
||||
if err != nil || seq != expectedSeq {
|
||||
return 0, fmt.Errorf("bad CPUSH sequence: %q", resp)
|
||||
}
|
||||
n, err := strconv.Atoi(fields[2])
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("bad CPUSH length: %q", resp)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
total := 0
|
||||
consecutiveMinFailures := 0
|
||||
|
||||
for len(p) > 0 {
|
||||
size := c.upSizer.Current()
|
||||
n := size
|
||||
if len(p) < n {
|
||||
n = len(p)
|
||||
}
|
||||
|
||||
seq := c.upSeq
|
||||
prefix := []byte(fmt.Sprintf("CPUSH %s %s %d ", c.token, c.sid, seq))
|
||||
payload := make([]byte, len(prefix)+n)
|
||||
copy(payload, prefix)
|
||||
copy(payload[len(prefix):], p[:n])
|
||||
|
||||
resp, err := c.pushLane.Do(payload)
|
||||
if err != nil {
|
||||
old, next := c.upSizer.Failure(size)
|
||||
if next == old && next == c.opts.minSize {
|
||||
consecutiveMinFailures++
|
||||
} else {
|
||||
consecutiveMinFailures = 0
|
||||
}
|
||||
if consecutiveMinFailures >= 8 {
|
||||
return total, fmt.Errorf("upload failed at minimum chunk %d: %w", next, err)
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "ERR ") {
|
||||
return total, fmt.Errorf("%s", resp)
|
||||
}
|
||||
|
||||
accepted, err := parseAck(resp, seq)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
if accepted > len(p) {
|
||||
return total, fmt.Errorf("server ACK length %d exceeds pending write %d", accepted, len(p))
|
||||
}
|
||||
|
||||
c.upSeq++
|
||||
total += accepted
|
||||
p = p[accepted:]
|
||||
consecutiveMinFailures = 0
|
||||
c.upSizer.Success(size)
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) Close() error {
|
||||
c.once.Do(func() {
|
||||
c.cancel()
|
||||
|
||||
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
||||
_, _ = doControl(lane, []byte(fmt.Sprintf("CCLOSE %s %s", c.token, c.sid)))
|
||||
lane.Close()
|
||||
|
||||
if c.pushLane != nil {
|
||||
c.pushLane.Close()
|
||||
}
|
||||
for _, lane := range c.pullLanes {
|
||||
lane.Close()
|
||||
}
|
||||
c.workers.Wait()
|
||||
close(c.results)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-chunk-local") }
|
||||
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-chunk-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-chunk" }
|
||||
func (d dummyAddr) String() string { return string(d) }
|
||||
@@ -1,478 +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", "change-this-token", "shared token")
|
||||
maxConnections = flag.Int("max-connections", 20000, "max simultaneous proxy connections")
|
||||
transport = flag.String("transport", "chunk", "transport: chunk (adaptive framed records), xor, or raw")
|
||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
||||
chunkStart = flag.Int("chunk-start", 256, "initial adaptive chunk payload bytes")
|
||||
chunkMin = flag.Int("chunk-min", 32, "minimum adaptive chunk payload bytes")
|
||||
chunkMax = flag.Int("chunk-max", 65536, "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", 64, "successful data records required before increasing chunk size")
|
||||
chunkAdaptLog = flag.Bool("chunk-adapt-log", false, "print adaptive chunk size changes")
|
||||
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
|
||||
chunkPollers = flag.Int("chunk-pollers", 16, "parallel downstream chunk pollers (1-128)")
|
||||
chunkReconnect = flag.Int("chunk-reconnect-every", 32, "reconnect each transaction lane after N requests; 0 keeps it open")
|
||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 5*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 != "raw" && *transport != "xor" && *transport != "chunk" {
|
||||
fmt.Fprintln(os.Stderr, "--transport must be chunk, xor, or raw")
|
||||
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)
|
||||
}
|
||||
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,498 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
type chunkSession struct {
|
||||
id string
|
||||
target net.Conn
|
||||
maxChunk int
|
||||
maxChunks int
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
chunks map[uint64][]byte
|
||||
nextDown uint64
|
||||
eof bool
|
||||
closed bool
|
||||
lastSeen time.Time
|
||||
debug *serverDebug
|
||||
|
||||
upMu sync.Mutex
|
||||
expectedUp uint64
|
||||
lastUpSeq uint64
|
||||
lastUpLen int
|
||||
haveLastUp bool
|
||||
}
|
||||
|
||||
func newChunkSession(id string, target net.Conn, maxChunk, maxChunks int, debug *serverDebug) *chunkSession {
|
||||
s := &chunkSession{
|
||||
id: id,
|
||||
target: target,
|
||||
maxChunk: maxChunk,
|
||||
maxChunks: maxChunks,
|
||||
notify: make(chan struct{}),
|
||||
chunks: make(map[uint64][]byte, maxChunks),
|
||||
lastSeen: time.Now(),
|
||||
debug: debug,
|
||||
}
|
||||
go s.readTarget()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *chunkSession) signalLocked() {
|
||||
close(s.notify)
|
||||
s.notify = make(chan struct{})
|
||||
}
|
||||
|
||||
func (s *chunkSession) touchLocked() {
|
||||
s.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (s *chunkSession) touch() {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *chunkSession) readTarget() {
|
||||
buf := make([]byte, s.maxChunk)
|
||||
|
||||
for {
|
||||
n, err := s.target.Read(buf)
|
||||
if n > 0 {
|
||||
data := append([]byte(nil), buf[:n]...)
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesDown.Add(uint64(n))
|
||||
}
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(s.chunks) < s.maxChunks {
|
||||
seq := s.nextDown
|
||||
s.nextDown++
|
||||
s.chunks[seq] = data
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
break
|
||||
}
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.logf("TARGET EOF session=%s err=%v", s.id, err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
if !s.closed {
|
||||
s.eof = true
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// push is idempotent for the most recently accepted sequence. This matters
|
||||
// when the server receives a record but the tiny ACK is lost: the client can
|
||||
// retry the same sequence at a smaller adaptive size without duplicating bytes
|
||||
// in the target stream. The ACK reports the length that was actually accepted.
|
||||
func (s *chunkSession) push(seq uint64, data []byte) (int, error) {
|
||||
s.upMu.Lock()
|
||||
defer s.upMu.Unlock()
|
||||
|
||||
if len(data) == 0 || len(data) > s.maxChunk {
|
||||
return 0, fmt.Errorf("upload record size %d is invalid", len(data))
|
||||
}
|
||||
|
||||
if s.haveLastUp && seq == s.lastUpSeq {
|
||||
s.touch()
|
||||
return s.lastUpLen, nil
|
||||
}
|
||||
|
||||
if seq < s.expectedUp {
|
||||
return 0, fmt.Errorf("upload sequence %d is too old", seq)
|
||||
}
|
||||
if seq > s.expectedUp {
|
||||
return 0, fmt.Errorf("unexpected upload sequence %d, expected %d", seq, s.expectedUp)
|
||||
}
|
||||
|
||||
if _, err := s.target.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesUp.Add(uint64(len(data)))
|
||||
s.debug.pushRecords.Add(1)
|
||||
}
|
||||
|
||||
s.lastUpSeq = seq
|
||||
s.lastUpLen = len(data)
|
||||
s.haveLastUp = true
|
||||
s.expectedUp++
|
||||
s.touch()
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// pull returns at most limit bytes from the requested stored chunk, beginning
|
||||
// at offset. The chunk sequence stays stable while the client retries smaller
|
||||
// fragments, so a large queued chunk can always be recovered after an MTU-like
|
||||
// failure without reopening the proxied destination connection.
|
||||
func (s *chunkSession) pull(want uint64, ack int64, offset, limit int, wait time.Duration) (data []byte, total int, eof bool, final uint64, waitExpired bool, err error) {
|
||||
if offset < 0 || limit <= 0 || limit > s.maxChunk {
|
||||
return nil, 0, false, 0, false, fmt.Errorf("invalid pull offset/limit")
|
||||
}
|
||||
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
|
||||
if ack >= 0 {
|
||||
removed := false
|
||||
for seq := range s.chunks {
|
||||
if seq <= uint64(ack) {
|
||||
delete(s.chunks, seq)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
s.signalLocked()
|
||||
}
|
||||
}
|
||||
|
||||
if chunk, ok := s.chunks[want]; ok {
|
||||
if offset >= len(chunk) {
|
||||
s.mu.Unlock()
|
||||
return nil, len(chunk), false, 0, false, fmt.Errorf("pull offset %d beyond chunk size %d", offset, len(chunk))
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(chunk) {
|
||||
end = len(chunk)
|
||||
}
|
||||
out := append([]byte(nil), chunk[offset:end]...)
|
||||
total = len(chunk)
|
||||
s.mu.Unlock()
|
||||
return out, total, false, 0, false, nil
|
||||
}
|
||||
|
||||
if s.eof && want >= s.nextDown {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
if s.closed {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
continue
|
||||
case <-timer.C:
|
||||
return nil, 0, false, 0, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chunkSession) close() {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
_ = s.target.Close()
|
||||
}
|
||||
|
||||
type chunkManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*chunkSession
|
||||
timeout time.Duration
|
||||
debug *serverDebug
|
||||
}
|
||||
|
||||
func newChunkManager(timeout time.Duration, debug *serverDebug) *chunkManager {
|
||||
m := &chunkManager{
|
||||
sessions: make(map[string]*chunkSession),
|
||||
timeout: timeout,
|
||||
debug: debug,
|
||||
}
|
||||
go m.cleanupLoop()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *chunkManager) get(id string) *chunkSession {
|
||||
m.mu.RLock()
|
||||
s := m.sessions[id]
|
||||
m.mu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *chunkManager) count() int {
|
||||
m.mu.RLock()
|
||||
n := len(m.sessions)
|
||||
m.mu.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *chunkManager) add(id string, s *chunkSession) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, exists := m.sessions[id]; exists {
|
||||
return fmt.Errorf("session already exists")
|
||||
}
|
||||
m.sessions[id] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *chunkManager) remove(id string) {
|
||||
m.mu.Lock()
|
||||
s := m.sessions[id]
|
||||
delete(m.sessions, id)
|
||||
m.mu.Unlock()
|
||||
if s != nil {
|
||||
s.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *chunkManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
cutoff := time.Now().Add(-m.timeout)
|
||||
var stale []string
|
||||
|
||||
m.mu.RLock()
|
||||
for id, s := range m.sessions {
|
||||
s.mu.Lock()
|
||||
last := s.lastSeen
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if closed || last.Before(cutoff) {
|
||||
stale = append(stale, id)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, id := range stale {
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.logf("SESSION timeout-close id=%s active_sessions=%d", id, m.count())
|
||||
}
|
||||
m.remove(id)
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.sessionsClosed.Add(1)
|
||||
m.debug.activeSessions.Add(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isChunkCommand(payload []byte) bool {
|
||||
return bytes.HasPrefix(payload, []byte("COPEN ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPUSH ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPULL ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CCLOSE "))
|
||||
}
|
||||
|
||||
func processChunkCommand(
|
||||
conn net.Conn,
|
||||
requestID uint32,
|
||||
payload []byte,
|
||||
token string,
|
||||
allowPrivate bool,
|
||||
cache *dnsCache,
|
||||
tcpBuffer int,
|
||||
manager *chunkManager,
|
||||
maxChunk int,
|
||||
maxBufferedChunks int,
|
||||
pollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) error {
|
||||
if bytes.HasPrefix(payload, []byte("COPEN ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad COPEN"))
|
||||
}
|
||||
if !tokenEqual(parts[1], token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := parts[2]
|
||||
if len(sid) < 16 || len(sid) > 64 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid session id"))
|
||||
}
|
||||
host := parts[3]
|
||||
port, err := strconv.Atoi(parts[4])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, host, port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
|
||||
session := newChunkSession(sid, target, maxChunk, maxBufferedChunks, debug)
|
||||
if err := manager.add(sid, session); err != nil {
|
||||
session.close()
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("COPEN session=%s target=%s:%d failed: %v", sid, host, port, err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsOpened.Add(1)
|
||||
debug.activeSessions.Add(1)
|
||||
debug.logf("SESSION OPEN id=%s peer=%v target=%s:%d max_chunk=%d active_sessions=%d", sid, conn.RemoteAddr(), host, port, maxChunk, manager.count())
|
||||
debug.chunkf("COPEN id=%s target=%s:%d -> OPENED max=%d", sid, host, port, maxChunk)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("OPENED %d", maxChunk)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPUSH ")) {
|
||||
parts := bytes.SplitN(payload, []byte(" "), 5)
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPUSH"))
|
||||
}
|
||||
if !tokenEqual(string(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := string(parts[2])
|
||||
seq, err := strconv.ParseUint(string(parts[3]), 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid sequence"))
|
||||
}
|
||||
s := manager.get(sid)
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
accepted, err := s.push(seq, parts[4])
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("CPUSH id=%s seq=%d bytes=%d: %v", sid, seq, len(parts[4]), err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil {
|
||||
debug.chunkf("CPUSH id=%s seq=%d bytes=%d -> ACK accepted=%d", sid, seq, len(parts[4]), accepted)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("ACK %d %d", seq, accepted)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPULL ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 7 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPULL"))
|
||||
}
|
||||
if !tokenEqual(parts[1], token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
s := manager.get(parts[2])
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
ack, err := strconv.ParseInt(parts[3], 10, 64)
|
||||
if err != nil || ack < -1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid ack"))
|
||||
}
|
||||
want, err := strconv.ParseUint(parts[4], 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid want"))
|
||||
}
|
||||
offset, err := strconv.Atoi(parts[5])
|
||||
if err != nil || offset < 0 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid offset"))
|
||||
}
|
||||
limit, err := strconv.Atoi(parts[6])
|
||||
if err != nil || limit < 1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid limit"))
|
||||
}
|
||||
if limit > maxChunk {
|
||||
limit = maxChunk
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.pullRequests.Add(1)
|
||||
debug.chunkf("CPULL id=%s ack=%d want=%d offset=%d limit=%d", parts[2], ack, want, offset, limit)
|
||||
}
|
||||
|
||||
data, total, eof, final, waitExpired, err := s.pull(want, ack, offset, limit, pollWait)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if waitExpired {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.waitRecords.Add(1)
|
||||
debug.chunkf("CPULL id=%s want=%d -> WAIT", parts[2], want)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("WAIT"))
|
||||
}
|
||||
if eof {
|
||||
if debug != nil {
|
||||
debug.chunkf("CPULL id=%s want=%d -> EOF final=%d", parts[2], want, final)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("EOF %d", final)))
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.dataRecords.Add(1)
|
||||
debug.chunkf("DATA id=%s seq=%d offset=%d bytes=%d total=%d", parts[2], want, offset, len(data), total)
|
||||
}
|
||||
prefix := []byte(fmt.Sprintf("DATA %d %d %d ", want, offset, total))
|
||||
out := make([]byte, len(prefix)+len(data))
|
||||
copy(out, prefix)
|
||||
copy(out[len(prefix):], data)
|
||||
return protocol.WriteResponseFrame(conn, requestID, out)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CCLOSE ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 3 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CCLOSE"))
|
||||
}
|
||||
if !tokenEqual(parts[1], token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
manager.remove(parts[2])
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsClosed.Add(1)
|
||||
debug.activeSessions.Add(-1)
|
||||
debug.logf("SESSION CLOSE id=%s peer=%v active_sessions=%d", parts[2], conn.RemoteAddr(), manager.count())
|
||||
debug.chunkf("CCLOSE id=%s -> CLOSED", parts[2])
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("CLOSED"))
|
||||
}
|
||||
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown chunk command"))
|
||||
}
|
||||
@@ -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,367 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
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 *chunkManager,
|
||||
chunkMax int,
|
||||
chunkBuffered 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(20 * time.Second))
|
||||
|
||||
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled && err != io.EOF {
|
||||
debug.errorf("peer=%v read request: %v", conn.RemoteAddr(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isChunkCommand(payload) {
|
||||
if err := processChunkCommand(
|
||||
conn,
|
||||
requestID,
|
||||
payload,
|
||||
token,
|
||||
allowPrivate,
|
||||
cache,
|
||||
tcpBuffer,
|
||||
manager,
|
||||
chunkMax,
|
||||
chunkBuffered,
|
||||
chunkPollWait,
|
||||
debug,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(string(payload))
|
||||
transport := "xor"
|
||||
|
||||
if len(parts) == 4 && parts[0] == "TUNNEL" {
|
||||
transport = "xor"
|
||||
} else if len(parts) == 5 && parts[0] == "TUNNEL2" {
|
||||
transport = strings.ToLower(parts[4])
|
||||
if transport != "raw" && transport != "xor" {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR transport must be RAW or XOR"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR expected TUNNEL, TUNNEL2, or chunk command"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenEqual(parts[1], token) {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR authentication failed"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(parts[3])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR invalid port"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, parts[2], port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("TUNNEL target=%s:%d connect failed: %v", parts[2], port, err)
|
||||
}
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR "+err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
if err := protocol.WriteResponseFrame(conn, requestID, []byte("CONNECTED")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
if transport == "raw" {
|
||||
protocol.RelayRaw(conn, target)
|
||||
} else {
|
||||
protocol.RelayXOR(conn, target)
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL closed peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
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", "change-this-token", "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", 65536, "maximum adaptive chunk payload bytes (32 bytes to 1 MiB)")
|
||||
chunkBuffered = flag.Int("chunk-buffered", 256, "maximum buffered destination chunks per 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)
|
||||
manager := newChunkManager(*sessionTimeout, debug)
|
||||
fmt.Printf("adaptive_chunk_max=%d buffered_chunks=%d poll_wait=%s\n", *chunkMax, *chunkBuffered, 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,
|
||||
*chunkBuffered,
|
||||
*chunkPollWait,
|
||||
debug,
|
||||
)
|
||||
default:
|
||||
if debug.enabled {
|
||||
debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
|
||||
}
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user