This commit is contained in:
2026-08-16 02:33:07 -03:00
parent 14beee38b0
commit 6dac260155
33 changed files with 2065 additions and 2301 deletions
+241 -118
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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>
+23
View File
@@ -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.
+1
View File
@@ -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.
+5
View File
@@ -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"
+9
View File
@@ -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"
+513
View File
@@ -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()
}
+743
View File
@@ -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()
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module dragontcpvpn
go 1.22
+267
View File
@@ -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
}
-748
View File
@@ -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) }
-478
View File
@@ -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()
}
}
}
-498
View File
@@ -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"))
}
-83
View File
@@ -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(),
)
}
}
-367
View File
@@ -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()
}
}
}
-3
View File
@@ -1,3 +0,0 @@
module dragontcp
go 1.22