diff --git a/README.md b/README.md
index 4324508..41151e4 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/SHA256SUMS b/SHA256SUMS
index e2b7746..08f7acd 100644
--- a/SHA256SUMS
+++ b/SHA256SUMS
@@ -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
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 0000000..89610a1
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/build_apk.sh b/android/build_apk.sh
new file mode 100644
index 0000000..3bc20b5
--- /dev/null
+++ b/android/build_apk.sh
@@ -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"
diff --git a/android/lib/arm64-v8a/libdragontcp_vpn.so b/android/lib/arm64-v8a/libdragontcp_vpn.so
new file mode 100644
index 0000000..2a4b79a
Binary files /dev/null and b/android/lib/arm64-v8a/libdragontcp_vpn.so differ
diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml
new file mode 100644
index 0000000..62f8bb8
--- /dev/null
+++ b/android/res/values/strings.xml
@@ -0,0 +1 @@
+DragonTCP VPN
diff --git a/android/src/com/dragontcp/client/DragonService.java b/android/src/com/dragontcp/client/DragonService.java
new file mode 100644
index 0000000..c6601d0
--- /dev/null
+++ b/android/src/com/dragontcp/client/DragonService.java
@@ -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 cmd=new ArrayList();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()=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();}
+}
diff --git a/android/src/com/dragontcp/client/MainActivity.java b/android/src/com/dragontcp/client/MainActivity.java
new file mode 100644
index 0000000..5bdf7e8
--- /dev/null
+++ b/android/src/com/dragontcp/client/MainActivity.java
@@ -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(offlogScroll.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();}
+}
diff --git a/bin/dragontcp-client-android-arm64 b/bin/dragontcp-client-android-arm64
deleted file mode 100644
index 648dd2c..0000000
Binary files a/bin/dragontcp-client-android-arm64 and /dev/null differ
diff --git a/bin/dragontcp-client-arm64 b/bin/dragontcp-client-arm64
deleted file mode 100644
index 7c89ac5..0000000
Binary files a/bin/dragontcp-client-arm64 and /dev/null differ
diff --git a/bin/dragontcp-client-armv7 b/bin/dragontcp-client-armv7
deleted file mode 100644
index 968b301..0000000
Binary files a/bin/dragontcp-client-armv7 and /dev/null differ
diff --git a/bin/dragontcp-client-linux-amd64 b/bin/dragontcp-client-linux-amd64
deleted file mode 100644
index ff8d550..0000000
Binary files a/bin/dragontcp-client-linux-amd64 and /dev/null differ
diff --git a/bin/dragontcp-server-arm64 b/bin/dragontcp-server-arm64
deleted file mode 100644
index 7309a5e..0000000
Binary files a/bin/dragontcp-server-arm64 and /dev/null differ
diff --git a/bin/dragontcp-server-linux-amd64 b/bin/dragontcp-server-linux-amd64
deleted file mode 100644
index c3244da..0000000
Binary files a/bin/dragontcp-server-linux-amd64 and /dev/null differ
diff --git a/bin/dragontcp-vpn-client-linux-amd64 b/bin/dragontcp-vpn-client-linux-amd64
new file mode 100644
index 0000000..93d4b22
Binary files /dev/null and b/bin/dragontcp-vpn-client-linux-amd64 differ
diff --git a/bin/dragontcp-vpn-server-linux-amd64 b/bin/dragontcp-vpn-server-linux-amd64
new file mode 100644
index 0000000..2fb4ef5
Binary files /dev/null and b/bin/dragontcp-vpn-server-linux-amd64 differ
diff --git a/bin/dragontcp-vpn-server-linux-arm64 b/bin/dragontcp-vpn-server-linux-arm64
new file mode 100644
index 0000000..80182f4
Binary files /dev/null and b/bin/dragontcp-vpn-server-linux-arm64 differ
diff --git a/build_all.sh b/build_all.sh
new file mode 100644
index 0000000..29013ca
--- /dev/null
+++ b/build_all.sh
@@ -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"
diff --git a/build_core.sh b/build_core.sh
new file mode 100644
index 0000000..ab6436f
--- /dev/null
+++ b/build_core.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")" && pwd)"
+cd "$ROOT/core"
+mkdir -p "$ROOT/bin" "$ROOT/android/lib/arm64-v8a"
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o "$ROOT/bin/dragontcp-vpn-server-linux-amd64" ./cmd/dragontcp-vpn-server
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o "$ROOT/bin/dragontcp-vpn-server-linux-arm64" ./cmd/dragontcp-vpn-server
+CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o "$ROOT/android/lib/arm64-v8a/libdragontcp_vpn.so" ./cmd/dragontcp-vpn-client
+echo "Built DragonTCP VPN server + Android core"
diff --git a/core/cmd/dragontcp-vpn-client/main.go b/core/cmd/dragontcp-vpn-client/main.go
new file mode 100644
index 0000000..23d3922
--- /dev/null
+++ b/core/cmd/dragontcp-vpn-client/main.go
@@ -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()
+}
diff --git a/core/cmd/dragontcp-vpn-server/main.go b/core/cmd/dragontcp-vpn-server/main.go
new file mode 100644
index 0000000..95e6605
--- /dev/null
+++ b/core/cmd/dragontcp-vpn-server/main.go
@@ -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()
+ }
+ }
+}
diff --git a/core/go.mod b/core/go.mod
new file mode 100644
index 0000000..5de27cd
--- /dev/null
+++ b/core/go.mod
@@ -0,0 +1,3 @@
+module dragontcpvpn
+
+go 1.22
diff --git a/go/internal/protocol/protocol.go b/core/internal/protocol/protocol.go
similarity index 100%
rename from go/internal/protocol/protocol.go
rename to core/internal/protocol/protocol.go
diff --git a/core/internal/protocol/vpnproto.go b/core/internal/protocol/vpnproto.go
new file mode 100644
index 0000000..6331bf0
--- /dev/null
+++ b/core/internal/protocol/vpnproto.go
@@ -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
+}
diff --git a/go/internal/protocol/xor_fast32.go b/core/internal/protocol/xor_fast32.go
similarity index 100%
rename from go/internal/protocol/xor_fast32.go
rename to core/internal/protocol/xor_fast32.go
diff --git a/go/internal/protocol/xor_fast64.go b/core/internal/protocol/xor_fast64.go
similarity index 100%
rename from go/internal/protocol/xor_fast64.go
rename to core/internal/protocol/xor_fast64.go
diff --git a/go/internal/protocol/xor_generic.go b/core/internal/protocol/xor_generic.go
similarity index 100%
rename from go/internal/protocol/xor_generic.go
rename to core/internal/protocol/xor_generic.go
diff --git a/go/cmd/dragontcp-client/chunk.go b/go/cmd/dragontcp-client/chunk.go
deleted file mode 100644
index add7507..0000000
--- a/go/cmd/dragontcp-client/chunk.go
+++ /dev/null
@@ -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) }
diff --git a/go/cmd/dragontcp-client/main.go b/go/cmd/dragontcp-client/main.go
deleted file mode 100644
index 5944d50..0000000
--- a/go/cmd/dragontcp-client/main.go
+++ /dev/null
@@ -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()
- }
- }
-}
diff --git a/go/cmd/dragontcp-server/chunk.go b/go/cmd/dragontcp-server/chunk.go
deleted file mode 100644
index 0b9546d..0000000
--- a/go/cmd/dragontcp-server/chunk.go
+++ /dev/null
@@ -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"))
-}
diff --git a/go/cmd/dragontcp-server/debug.go b/go/cmd/dragontcp-server/debug.go
deleted file mode 100644
index f23f94b..0000000
--- a/go/cmd/dragontcp-server/debug.go
+++ /dev/null
@@ -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(),
- )
- }
-}
diff --git a/go/cmd/dragontcp-server/main.go b/go/cmd/dragontcp-server/main.go
deleted file mode 100644
index dce7cc9..0000000
--- a/go/cmd/dragontcp-server/main.go
+++ /dev/null
@@ -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()
- }
- }
-}
diff --git a/go/go.mod b/go/go.mod
deleted file mode 100644
index a8d2a13..0000000
--- a/go/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module dragontcp
-
-go 1.22