diff --git a/README.md b/README.md index 41151e4..b9488f1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -# DragonTCP Full Android VPN v1 +# DragonTCP VPN v3 -This build replaces the old HTTP-proxy-only Android design with a real layer-3 -VPN packet tunnel. +DragonTCP v3 is a real Android layer-3 VPN over DragonTCP's adaptive XOR-framed +TCP transport. It captures IPv4 and IPv6 through Android `VpnService`, passes +the TUN file descriptor to the Go core, and transfers raw IP packets to a Linux +DragonTCP server listening on TCP/53. -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. +This release fixes two important problems from the previous packet-VPN build: + +1. Android can emit IPv6 link-local/control packets such as `fe80::...` on the + VPN TUN. Those packets no longer terminate the DragonTCP session. The client + drops packets whose source is not the assigned DragonTCP VPN address, and the + server independently treats source-mismatch/control packets as non-fatal + drops. +2. The DragonTCP transport chunk ceiling is restored to **1 MiB (1,048,576 + bytes)**. Raw IP packets remain limited to 65,535 bytes, but multiple TUN + packets are batched into transfer objects up to 1 MiB so chunk sizes above + the VPN MTU are actually useful. ## Architecture @@ -18,59 +26,58 @@ Android apps v Android VpnService TUN (MTU 1280) | + | raw IPv4/IPv6 packets v -DragonTCP Go VPN core +DragonTCP Android Go core | - | adaptive small records, XOR 0xAD, TCP/53 + | packet batching (up to 1 MiB transfer objects) + | adaptive fragmentation 32 B .. 1 MiB + | XOR 0xAD framing v -DragonTCP VPN server +TCP/53 + | + v +DragonTCP Linux server | v Linux TUN dragontcp0 | - | IP forwarding + NAT + | forwarding / NAT v Internet ``` -Because complete IP packets are tunneled, this carries TCP, UDP, DNS, ICMP, -IPv4 and IPv6. Applications do not need HTTP or SOCKS proxy support. +Because the tunnel carries raw IP packets, it can carry TCP, UDP, DNS, ICMP, +IPv4 and IPv6. It does not depend on applications supporting an HTTP proxy. ## Included files ```text bin/dragontcp-vpn-server-linux-amd64 bin/dragontcp-vpn-server-linux-arm64 +bin/dragontcp-vpn-client-linux-amd64 # test/debug client android/build/DragonTCP-VPN.apk android/lib/arm64-v8a/libdragontcp_vpn.so -core/ complete Go source -android/src/ complete Android Java source +core/ # complete Go source +android/src/ # complete Android Java source build_core.sh build_all.sh android/build_apk.sh ``` -## Server requirements +## Server -The full VPN server needs root/CAP_NET_ADMIN because it creates a Linux TUN -interface and enables packet forwarding/NAT. +The server needs root or equivalent CAP_NET_ADMIN permissions because it +creates a Linux TUN and configures forwarding/NAT. -Install the normal Linux networking tools if they are not already present: +Install networking tools on Debian/Ubuntu if needed: ```bash sudo apt-get update sudo apt-get install -y iproute2 iptables ``` -TCP port 53 must be free. - -Check: - -```bash -sudo ss -lntp | grep ':53' -``` - -## Start the server +Start: ```bash sudo ./dragontcp-vpn-server-linux-amd64 \ @@ -78,130 +85,125 @@ sudo ./dragontcp-vpn-server-linux-amd64 \ --debug ``` -The defaults are: +Defaults: ```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 +listen TCP port 53 +server chunk max 1048576 bytes (1 MiB) +transfer batch max 1048576 bytes (1 MiB) +batch delay 1ms +TUN MTU 1280 +server IPv4 10.123.0.1/16 +server IPv6 fd7a:4472:6167:6f6e::1/64 +poll wait 100ms +queued packet limit 2048/client +queued byte limit 8 MiB/client +auto NAT enabled ``` -The server automatically enables IPv4/IPv6 forwarding and installs -MASQUERADE/forward rules with `iptables`/`ip6tables` when available. - -If you manage routing/NAT yourself: - -```bash -sudo ./dragontcp-vpn-server-linux-amd64 \ - --token 'YOUR_SECRET' \ - --auto-nat=false -``` - -To allow clients to reach private/LAN destination addresses too: - -```bash ---allow-private -``` - -## Debug server - -Normal diagnostics: +Useful explicit command: ```bash sudo ./dragontcp-vpn-server-linux-amd64 \ --token 'YOUR_SECRET' \ + --chunk-max 1048576 \ + --vpn-buffer-bytes 8388608 \ + --batch-delay 1ms \ --debug \ --debug-stats-interval 5s ``` -Very verbose per-IP-packet diagnostics: +Per-packet/batch diagnostics are very verbose: ```bash --debug-packets ``` -Do not leave `--debug-packets` enabled for high-throughput use. - ## Android app -Install: +Install `android/build/DragonTCP-VPN.apk`. -```text -DragonTCP-VPN.apk -``` - -The UI is intentionally small: +The UI asks for: ```text Server -TCP Port +TCP port Token -Maximum fragment -Minimum fragment +Maximum transport fragment +Minimum transport fragment Timeout - -CONNECT -STOP - -Live log ``` Defaults: ```text Port 53 -Max 1280 +Max 1048576 Min 32 Timeout 2s Pollers 1 (fixed) -MTU 1280 (fixed) +VPN MTU 1280 ``` -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: +The adaptive record starts at Max and shrinks after transport failures. The +1 MiB value is a DragonTCP transport ceiling, not the IP MTU. + +### Why 1 MiB can now help even though the VPN MTU is 1280 + +The old packet-VPN sent one TUN packet per DragonTCP transfer object, so a +record size larger than the IP packet had no benefit. v3 batches adjacent TUN +packets for a short window: ```text -1280 -> 640 -> 320 -> 160 -> 80 -> 40 -> 32 +1280-byte packet --+ +1280-byte packet ---+ +1280-byte packet ----+--> one DragonTCP transfer object --> adaptive fragments +... | +1280-byte packet ----+ ``` -After sustained successful full-size records it cautiously grows again. +A busy flow can therefore produce transfer objects much larger than 65,535 +bytes. If the network accepts large DragonTCP records, fewer transactions are +needed. If it does not, the same transfer object is automatically fragmented +into smaller records and retried. -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. +## Android link-local source fix -## Why Max defaults to 1280 +A log such as this from the previous build: -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. +```text +VPN stopped: source fe80::... does not match session address +``` -## Building everything from source +is no longer fatal. + +The Android core now logs an occasional line such as: + +```text +VPN DROP local packet (source fe80::... is not assigned VPN address) dropped=1 +``` + +and continues running. The server also performs a non-fatal drop as a second +line of defense. + +## Build from source Requirements: - Go 1.22+ - JDK 17+ -- Android SDK platform and build-tools -- `zip` +- Android SDK platform/build-tools +- zip -No Android NDK is required in this build. +No Android NDK is required for this build. -Set the SDK path: +Set the SDK directory: ```bash export ANDROID_SDK_ROOT="$HOME/Android/Sdk" ``` -Build server, Android native core, and APK: +Build native components and APK: ```bash ./build_all.sh @@ -216,76 +218,31 @@ android/lib/arm64-v8a/libdragontcp_vpn.so android/build/DragonTCP-VPN.apk ``` -Build only Go/native components: +Build only the Go/native components: ```bash ./build_core.sh ``` -Build only APK after the core is present: +Build only the APK after the native core exists: ```bash -./android/build_apk.sh +cd android +./build_apk.sh ``` -## Android TUN fd handoff +## Testing performed -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. +The Go packages compile with `go test ./...`. -This avoids JNI and avoids passing an inherited descriptor through -`ProcessBuilder`. +A local mock-TUN test verified: -## Protocol packet mode +- an IPv6 `fe80::` source packet is dropped without terminating the client; +- a valid packet immediately afterward still passes; +- 120 IPv4 packets were combined into a **120,241-byte transfer object**, + proving that transfer objects larger than 65,535 bytes work; +- the echoed packets were returned byte-for-byte and in order; +- the same path also works with a fixed **32-byte DragonTCP fragment size**. -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. +A physical Android phone is still required to validate device/vendor-specific +`VpnService` behavior and the real mobile-network TCP/53 path. diff --git a/SHA256SUMS b/SHA256SUMS index 08f7acd..f862aeb 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,4 +1,4 @@ -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 +26b46523f9100ebb7c84a606cfad37d5d17e45a1c6eb1571340f7b5f74668f3e /mnt/data/DragonTCP-VPN-v3/android/build/DragonTCP-VPN.apk +2e70feec6ea544efc9a3b3ea8c66a205c4827176d80374fa2b4b48845ec09e2e /mnt/data/DragonTCP-VPN-v3/bin/dragontcp-vpn-server-linux-amd64 +c049c8449b01d46049c2cf4a0368d23562219025d38aa9024ea3966c15dcc3b0 /mnt/data/DragonTCP-VPN-v3/bin/dragontcp-vpn-server-linux-arm64 +c0f600c1cff7bc7809c2afab0272fe528d36a7cac6815820e8fd454e81b0894c /mnt/data/DragonTCP-VPN-v3/android/lib/arm64-v8a/libdragontcp_vpn.so diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 89610a1..1838c6f 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="4" + android:versionName="3.0"> diff --git a/android/src/com/dragontcp/client/DragonService.java b/android/src/com/dragontcp/client/DragonService.java index c6601d0..ff21d0c 100644 --- a/android/src/com/dragontcp/client/DragonService.java +++ b/android/src/com/dragontcp/client/DragonService.java @@ -43,7 +43,7 @@ public class DragonService extends VpnService { 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); + int port=intent.getIntExtra("port",53),max=intent.getIntExtra("chunkMax",1048576),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{ diff --git a/android/src/com/dragontcp/client/MainActivity.java b/android/src/com/dragontcp/client/MainActivity.java index 5bdf7e8..531f62d 100644 --- a/android/src/com/dragontcp/client/MainActivity.java +++ b/android/src/com/dragontcp/client/MainActivity.java @@ -96,13 +96,13 @@ public class MainActivity extends Activity { 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 void loadSettings(){server.setText(prefs.getString("server",""));port.setText(prefs.getString("port","53"));token.setText(prefs.getString("token",""));chunkMax.setText(prefs.getString("chunkMax","1048576"));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); + int p=intValue(port,53),min=intValue(chunkMin,32),max=intValue(chunkMax,1048576); 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(min<32||max>1048576||min>max){toast("Chunks must satisfy 32 <= Min <= Max <= 1048576");return false;} if(timeout.getText().toString().trim().isEmpty()){toast("Enter a timeout such as 2s");return false;} return true; } @@ -116,7 +116,7 @@ public class MainActivity extends Activity { 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); + int max=intValue(chunkMax,1048576),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; diff --git a/core/cmd/dragontcp-vpn-client/main.go b/core/cmd/dragontcp-vpn-client/main.go index 23d3922..8e0f3cc 100644 --- a/core/cmd/dragontcp-vpn-client/main.go +++ b/core/cmd/dragontcp-vpn-client/main.go @@ -238,20 +238,25 @@ type vpnClient struct { ipv4, ipv6 netip.Addr mtu int timeout time.Duration + batchDelay time.Duration reconnectEvery int upSizer, downSizer *adaptiveSizer control, upload, download *txnLane upPackets, downPackets, upBytes, downBytes atomic.Uint64 + upBatches, downBatches, localDropped 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) { +func newVPNClient(tun *os.File, addr, token string, v4, v6 netip.Addr, mtu, start, min, max, growAfter, reconnectEvery int, timeout, batchDelay 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, + if batchDelay < 0 { + batchDelay = 0 + } + return &vpnClient{tun: tun, sid: sid, serverAddr: addr, token: token, ipv4: v4, ipv6: v6, mtu: mtu, timeout: timeout, batchDelay: batchDelay, 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 } @@ -296,30 +301,113 @@ func (v *vpnClient) close() { }) } -func (v *vpnClient) uploadLoop(errs chan<- error) { - buf := make([]byte, 65535) - var seq uint32 +func (v *vpnClient) logLocalDrop(reason string) { + n := v.localDropped.Add(1) + // Link-local/control traffic can be noisy. Keep it visible without filling + // the Android live log or making a harmless packet fatal to the VPN. + if n <= 8 || n%256 == 0 { + fmt.Printf("VPN DROP local packet (%s) dropped=%d\n", reason, n) + } +} + +func (v *vpnClient) tunReadLoop(out chan<- []byte, errs chan<- error) { + buf := make([]byte, protocol.VPNMaxPacket) for { n, err := v.tun.Read(buf) if err != nil { errs <- err return } - if n < 1 { + if n < 1 || n > protocol.VPNMaxPacket { continue } packet := append([]byte(nil), buf[:n]...) - if n > 65535 { + src, _, err := protocol.PacketAddresses(packet) + if err != nil { + v.logLocalDrop(err.Error()) continue } + if src != v.ipv4 && src != v.ipv6 { + v.logLocalDrop(fmt.Sprintf("source %s is not assigned VPN address", src)) + continue + } + select { + case out <- packet: + case <-v.stopped: + return + } + } +} + +func batchWireSize(packets [][]byte) int { + n := 1 + for _, p := range packets { + n += 2 + len(p) + } + return n +} + +func (v *vpnClient) uploadLoop(in <-chan []byte, errs chan<- error) { + var seq uint32 + var carry []byte + for { + var first []byte + if carry != nil { + first, carry = carry, nil + } else { + select { + case first = <-in: + case <-v.stopped: + return + } + } + + packets := [][]byte{first} + encodedSize := 1 + 2 + len(first) + timer := time.NewTimer(v.batchDelay) + collect: + for encodedSize < protocol.VPNMaxBatch { + select { + case p := <-in: + need := 2 + len(p) + if encodedSize+need > protocol.VPNMaxBatch { + carry = p + break collect + } + packets = append(packets, p) + encodedSize += need + case <-timer.C: + break collect + case <-v.stopped: + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + return + } + } + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + + batch, err := protocol.BuildVPNBatch(packets) + if err != nil { + errs <- err + return + } offset := 0 - for offset < n { + for offset < len(batch) { limit := v.upSizer.Current() - size := n - offset + size := len(batch) - offset if size > limit { size = limit } - req, e := protocol.BuildVPNPush(v.sid, seq, offset, n, packet[offset:offset+size]) + req, e := protocol.BuildVPNPush(v.sid, seq, offset, len(batch), batch[offset:offset+size]) if e != nil { errs <- e return @@ -334,16 +422,20 @@ func (v *vpnClient) uploadLoop(errs chan<- error) { errs <- e return } - if rseq != seq || accepted < offset || accepted > n { + if rseq != seq || accepted < offset || accepted > len(batch) { errs <- errors.New("bad server upload ACK") return } - fullRecord := size == limit - v.upSizer.Success(size, fullRecord) + v.upSizer.Success(size, size == limit) offset = accepted } - v.upPackets.Add(1) - v.upBytes.Add(uint64(n)) + var rawBytes uint64 + for _, p := range packets { + rawBytes += uint64(len(p)) + } + v.upPackets.Add(uint64(len(packets))) + v.upBytes.Add(rawBytes) + v.upBatches.Add(1) seq++ } } @@ -352,7 +444,7 @@ func (v *vpnClient) downloadLoop(errs chan<- error) { var want uint32 ack := protocol.VPNNoAck offset := 0 - var packet []byte + var transfer []byte total := 0 for { limit := v.downSizer.Current() @@ -374,42 +466,58 @@ func (v *vpnClient) downloadLoop(errs chan<- error) { if wait { continue } - if seq != want || roff != offset || rtotal < 1 || rtotal > 65535 { + if seq != want || roff != offset || rtotal < 1 || rtotal > protocol.VPNMaxBatch { errs <- errors.New("bad server download sequence") return } if offset == 0 { total = rtotal - packet = make([]byte, 0, total) + transfer = make([]byte, 0, total) } else if rtotal != total { - errs <- errors.New("download packet size changed") + errs <- errors.New("download transfer size changed") return } - packet = append(packet, data...) + transfer = append(transfer, 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") + errs <- errors.New("download transfer overflow") return } - n, e := v.tun.Write(packet) + packets, e := protocol.ParseVPNBatch(transfer) if e != nil { - errs <- e - return + // Compatibility with the first packet-VPN build, which used one raw + // IP packet as each transfer object. + if len(transfer) > 0 && (transfer[0]>>4 == 4 || transfer[0]>>4 == 6) { + packets = [][]byte{transfer} + } else { + errs <- e + return + } } - if n != len(packet) { - errs <- io.ErrShortWrite - return + var rawBytes uint64 + for _, packet := range packets { + n, e := v.tun.Write(packet) + if e != nil { + errs <- e + return + } + if n != len(packet) { + errs <- io.ErrShortWrite + return + } + rawBytes += uint64(n) } - v.downPackets.Add(1) - v.downBytes.Add(uint64(n)) + v.downPackets.Add(uint64(len(packets))) + v.downBytes.Add(rawBytes) + v.downBatches.Add(1) ack = want want++ offset = 0 - packet = nil + transfer = nil total = 0 } } @@ -419,8 +527,10 @@ func (v *vpnClient) run() error { return err } fmt.Println("VPN READY") - errs := make(chan error, 2) - go v.uploadLoop(errs) + errs := make(chan error, 3) + packets := make(chan []byte, 256) + go v.tunReadLoop(packets, errs) + go v.uploadLoop(packets, errs) go v.downloadLoop(errs) ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() @@ -429,7 +539,7 @@ func (v *vpnClient) run() error { 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()) + fmt.Printf("STATS up_packets=%d down_packets=%d up_batches=%d down_batches=%d up_bytes=%d down_bytes=%d local_dropped=%d upload_chunk=%d download_chunk=%d pollers=1\n", v.upPackets.Load(), v.downPackets.Load(), v.upBatches.Load(), v.downBatches.Load(), v.upBytes.Load(), v.downBytes.Load(), v.localDropped.Load(), v.upSizer.Current(), v.downSizer.Current()) case <-v.stopped: return nil } @@ -445,11 +555,12 @@ func main() { 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") + chunkMax := flag.Int("chunk-max", protocol.VPNMaxFragment, "maximum adaptive record bytes (up to 1 MiB)") 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") + chunkStart := flag.Int("chunk-start", protocol.VPNMaxFragment, "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") + batchDelay := flag.Duration("batch-delay", time.Millisecond, "maximum delay used to combine adjacent TUN packets into one transfer object") 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() @@ -496,7 +607,7 @@ func main() { } } addr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort)) - client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *adaptLog) + client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *batchDelay, *adaptLog) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/core/cmd/dragontcp-vpn-server/main.go b/core/cmd/dragontcp-vpn-server/main.go index 95e6605..4487dbf 100644 --- a/core/cmd/dragontcp-vpn-server/main.go +++ b/core/cmd/dragontcp-vpn-server/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "crypto/subtle" "encoding/hex" "errors" @@ -36,6 +37,8 @@ type debugStats struct { activeSessions atomic.Int64 upPackets atomic.Uint64 downPackets atomic.Uint64 + upBatches atomic.Uint64 + downBatches atomic.Uint64 upBytes atomic.Uint64 downBytes atomic.Uint64 dropped atomic.Uint64 @@ -69,20 +72,27 @@ func tokenEqual(a, b string) bool { } type vpnSession struct { - sid protocol.VPNSessionID - ipv4 netip.Addr - ipv6 netip.Addr - mtu int - maxChunk int - maxPackets int - manager *vpnManager + sid protocol.VPNSessionID + ipv4 netip.Addr + ipv6 netip.Addr + mtu int + maxChunk int + maxPackets int + maxQueueBytes int + batchDelay time.Duration + manager *vpnManager - mu sync.Mutex - notify chan struct{} - packets map[uint32][]byte - nextDown uint32 - closed bool - lastSeen time.Time + mu sync.Mutex + notify chan struct{} + packets map[uint32]*downTransfer + nextDown uint32 + closed bool + lastSeen time.Time + pendingPackets [][]byte + pendingEncoded int + pendingTimer *time.Timer + queuedPacketCount int + queuedBytes int upMu sync.Mutex expectedUp uint32 @@ -95,10 +105,16 @@ type vpnSession struct { haveLastComplete bool } -func newVPNSession(m *vpnManager, sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu, maxChunk, maxPackets int) *vpnSession { +type downTransfer struct { + data []byte + packetCount int + rawBytes int +} + +func newVPNSession(m *vpnManager, sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu, maxChunk, maxPackets, maxQueueBytes int, batchDelay time.Duration) *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(), + sid: sid, ipv4: v4, ipv6: v6, mtu: mtu, maxChunk: maxChunk, maxPackets: maxPackets, maxQueueBytes: maxQueueBytes, batchDelay: batchDelay, + manager: m, notify: make(chan struct{}), packets: make(map[uint32]*downTransfer, maxPackets), lastSeen: time.Now(), } } @@ -109,8 +125,54 @@ func (s *vpnSession) signalLocked() { func (s *vpnSession) touchLocked() { s.lastSeen = time.Now() } func (s *vpnSession) touch() { s.mu.Lock(); s.touchLocked(); s.mu.Unlock() } +func (s *vpnSession) flushPendingLocked() { + if len(s.pendingPackets) == 0 { + return + } + if s.pendingTimer != nil { + s.pendingTimer.Stop() + s.pendingTimer = nil + } + batch, err := protocol.BuildVPNBatch(s.pendingPackets) + if err != nil { + if s.manager.debug != nil { + s.manager.debug.dropped.Add(uint64(len(s.pendingPackets))) + s.manager.debug.errorf("BATCH sid=%s: %v", shortSID(s.sid), err) + } + s.queuedPacketCount -= len(s.pendingPackets) + for _, p := range s.pendingPackets { + s.queuedBytes -= len(p) + } + s.pendingPackets = nil + s.pendingEncoded = 0 + return + } + rawBytes := 0 + for _, p := range s.pendingPackets { + rawBytes += len(p) + } + seq := s.nextDown + s.nextDown++ + s.packets[seq] = &downTransfer{data: batch, packetCount: len(s.pendingPackets), rawBytes: rawBytes} + if s.manager.debug != nil { + s.manager.debug.downBatches.Add(1) + s.manager.debug.packetf("BATCH QUEUE sid=%s seq=%d packets=%d raw_bytes=%d transfer_bytes=%d", shortSID(s.sid), seq, len(s.pendingPackets), rawBytes, len(batch)) + } + s.pendingPackets = nil + s.pendingEncoded = 0 + s.signalLocked() +} + +func (s *vpnSession) flushPending() { + s.mu.Lock() + if !s.closed { + s.flushPendingLocked() + } + s.mu.Unlock() +} + func (s *vpnSession) enqueue(packet []byte) bool { - if len(packet) == 0 || len(packet) > 65535 { + if len(packet) == 0 || len(packet) > protocol.VPNMaxPacket { return false } s.mu.Lock() @@ -118,21 +180,39 @@ func (s *vpnSession) enqueue(packet []byte) bool { if s.closed { return false } - if len(s.packets) >= s.maxPackets { + need := 2 + len(packet) + if len(s.pendingPackets) > 0 && s.pendingEncoded+need > protocol.VPNMaxBatch { + s.flushPendingLocked() + } + if s.queuedPacketCount >= s.maxPackets || s.queuedBytes+len(packet) > s.maxQueueBytes { 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...) + p := append([]byte(nil), packet...) + if len(s.pendingPackets) == 0 { + s.pendingEncoded = 1 + } + s.pendingPackets = append(s.pendingPackets, p) + s.pendingEncoded += 2 + len(p) + s.queuedPacketCount++ + s.queuedBytes += len(p) 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)) + s.manager.debug.packetf("QUEUE sid=%s bytes=%d pending_packets=%d pending_transfer=%d", shortSID(s.sid), len(packet), len(s.pendingPackets), s.pendingEncoded) + } + if s.pendingEncoded >= protocol.VPNMaxBatch { + s.flushPendingLocked() + } else if s.pendingTimer == nil { + delay := s.batchDelay + if delay <= 0 { + s.flushPendingLocked() + } else { + s.pendingTimer = time.AfterFunc(delay, s.flushPending) + } } return true } @@ -140,8 +220,8 @@ func (s *vpnSession) enqueue(packet []byte) bool { 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 total < 1 || total > protocol.VPNMaxBatch || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total { + return 0, errors.New("invalid transfer fragment") } if s.haveLastComplete && seq == s.lastComplete { @@ -165,14 +245,14 @@ func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, erro s.currentBuf = make([]byte, 0, total) } if s.currentSeq != seq || s.currentTotal != total { - return 0, errors.New("packet fragment metadata changed") + return 0, errors.New("transfer 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) { + if end <= len(s.currentBuf) && bytes.Equal(s.currentBuf[offset:end], data) { return len(s.currentBuf), nil } return 0, errors.New("retry fragment does not match accepted data") @@ -188,11 +268,11 @@ func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, erro return accepted, nil } - packet := append([]byte(nil), s.currentBuf...) + transfer := append([]byte(nil), s.currentBuf...) s.haveCurrent = false s.currentBuf = nil - if err := s.manager.acceptClientPacket(s, packet); err != nil { + if err := s.manager.acceptClientTransfer(s, transfer); err != nil { return 0, err } @@ -202,9 +282,8 @@ func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, erro 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)) + s.manager.debug.upBatches.Add(1) + s.manager.debug.packetf("UP BATCH sid=%s seq=%d transfer_bytes=%d", shortSID(s.sid), seq, len(transfer)) } return accepted, nil } @@ -221,21 +300,26 @@ func (s *vpnSession) pull(ack, want uint32, offset, limit int, wait time.Duratio if ack != protocol.VPNNoAck { for seq := range s.packets { if seq <= ack { + rec := s.packets[seq] + if rec != nil { + s.queuedPacketCount -= rec.packetCount + s.queuedBytes -= rec.rawBytes + } delete(s.packets, seq) } } } - if packet, ok := s.packets[want]; ok { - if offset >= len(packet) { + if rec, ok := s.packets[want]; ok { + if offset >= len(rec.data) { s.mu.Unlock() - return nil, len(packet), false, errors.New("pull offset beyond packet") + return nil, len(rec.data), false, errors.New("pull offset beyond transfer") } end := offset + limit - if end > len(packet) { - end = len(packet) + if end > len(rec.data) { + end = len(rec.data) } - out := append([]byte(nil), packet[offset:end]...) - total := len(packet) + out := append([]byte(nil), rec.data[offset:end]...) + total := len(rec.data) s.mu.Unlock() return out, total, false, nil } @@ -257,35 +341,41 @@ func (s *vpnSession) close() { s.mu.Lock() if !s.closed { s.closed = true + if s.pendingTimer != nil { + s.pendingTimer.Stop() + s.pendingTimer = nil + } 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 + mu sync.RWMutex + sessions map[protocol.VPNSessionID]*vpnSession + byIPv4 map[netip.Addr]*vpnSession + byIPv6 map[netip.Addr]*vpnSession + maxChunk int + maxPackets int + maxQueueBytes int + batchDelay time.Duration + 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 { +func newVPNManager(tun *os.File, mockEcho bool, maxChunk, maxPackets, maxQueueBytes int, pollWait, timeout, batchDelay 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, + maxChunk: maxChunk, maxPackets: maxPackets, maxQueueBytes: maxQueueBytes, pollWait: pollWait, timeout: timeout, batchDelay: batchDelay, tun: tun, mockEcho: mockEcho, allowPrivate: allowPrivate, debug: debug, v4Prefix: v4p, v6Prefix: v6p, } if tun != nil { @@ -318,7 +408,7 @@ func (m *vpnManager) addOrGet(sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu 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) + s := newVPNSession(m, sid, v4, v6, mtu, m.maxChunk, m.maxPackets, m.maxQueueBytes, m.batchDelay) m.sessions[sid] = s m.byIPv4[v4] = s m.byIPv6[v6] = s @@ -376,40 +466,6 @@ func (m *vpnManager) cleanupLoop() { } } -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 @@ -423,32 +479,68 @@ func destinationAllowed(dst netip.Addr, allowPrivate bool) bool { return true } -func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) error { - src, dst, err := packetAddresses(packet) +func (m *vpnManager) dropClientPacket(s *vpnSession, packet []byte, reason string) { + if m.debug != nil { + m.debug.dropped.Add(1) + m.debug.packetf("DROP sid=%s bytes=%d reason=%s", shortSID(s.sid), len(packet), reason) + // A source mismatch can be normal Android link-local/control traffic. + // Never tear down the whole VPN session for one such packet. + m.debug.logf("DROP sid=%s reason=%s", shortSID(s.sid), reason) + } +} + +func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) (bool, error) { + src, dst, err := protocol.PacketAddresses(packet) if err != nil { - return err + m.dropClientPacket(s, packet, err.Error()) + return false, nil } if src != s.ipv4 && src != s.ipv6 { - return fmt.Errorf("source %s does not match session address", src) + m.dropClientPacket(s, packet, fmt.Sprintf("source %s does not match session address", src)) + return false, nil } if !destinationAllowed(dst, m.allowPrivate) { - return fmt.Errorf("destination %s is blocked; use --allow-private to permit it", dst) + m.dropClientPacket(s, packet, fmt.Sprintf("destination %s is blocked", dst)) + return false, nil } if m.mockEcho { s.enqueue(packet) - return nil + return true, nil } if m.tun == nil { - return errors.New("VPN TUN is unavailable") + return false, errors.New("VPN TUN is unavailable") } m.tunWriteMu.Lock() n, err := m.tun.Write(packet) m.tunWriteMu.Unlock() if err != nil { - return err + return false, err } if n != len(packet) { - return io.ErrShortWrite + return false, io.ErrShortWrite + } + return true, nil +} + +func (m *vpnManager) acceptClientTransfer(s *vpnSession, transfer []byte) error { + packets, err := protocol.ParseVPNBatch(transfer) + if err != nil { + // Compatibility with the first packet-VPN build. + if len(transfer) > 0 && (transfer[0]>>4 == 4 || transfer[0]>>4 == 6) { + packets = [][]byte{transfer} + } else { + return err + } + } + for _, packet := range packets { + accepted, err := m.acceptClientPacket(s, packet) + if err != nil { + return err + } + if accepted && m.debug != nil { + m.debug.upPackets.Add(1) + m.debug.upBytes.Add(uint64(len(packet))) + } } return nil } @@ -467,7 +559,7 @@ func (m *vpnManager) tunReadLoop() { continue } packet := append([]byte(nil), buf[:n]...) - _, dst, e := packetAddresses(packet) + _, dst, e := protocol.PacketAddresses(packet) if e != nil { continue } @@ -667,8 +759,10 @@ func main() { 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)") + maxChunk := flag.Int("chunk-max", protocol.VPNMaxFragment, "maximum DragonTCP transport fragment bytes (32-1048576)") maxPackets := flag.Int("vpn-buffered-packets", 2048, "maximum queued return IP packets per client") + maxQueueBytes := flag.Int("vpn-buffer-bytes", 8*1024*1024, "maximum queued raw return bytes per client") + batchDelay := flag.Duration("batch-delay", time.Millisecond, "maximum delay to combine adjacent TUN packets into one transfer object") 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") @@ -684,6 +778,10 @@ func main() { fmt.Fprintf(os.Stderr, "--chunk-max must be 32-%d\n", protocol.VPNMaxFragment) os.Exit(2) } + if *maxPackets < 1 || *maxQueueBytes < protocol.VPNMaxPacket { + fmt.Fprintln(os.Stderr, "invalid VPN buffer limits") + os.Exit(2) + } if *mtu < 576 || *mtu > 9000 { fmt.Fprintln(os.Stderr, "--mtu must be 576-9000") os.Exit(2) @@ -699,7 +797,7 @@ func main() { } defer tun.Close() } - manager := newVPNManager(tun, *mockEcho, *maxChunk, *maxPackets, *pollWait, *sessionTimeout, *allowPrivate, debug) + manager := newVPNManager(tun, *mockEcho, *maxChunk, *maxPackets, *maxQueueBytes, *pollWait, *sessionTimeout, *batchDelay, *allowPrivate, debug) addr := net.JoinHostPort(*host, strconv.Itoa(*port)) ln, err := net.Listen("tcp", addr) if err != nil { @@ -713,13 +811,13 @@ func main() { } 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) + fmt.Printf("chunk_max=%d batch_max=%d batch_delay=%s poll_wait=%s buffered_packets=%d buffer_bytes=%d\n", *maxChunk, protocol.VPNMaxBatch, batchDelay.String(), pollWait.String(), *maxPackets, *maxQueueBytes) 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()) + fmt.Printf("[DEBUG] STATS uptime=%s conns=%d sessions=%d up_packets=%d down_packets=%d up_batches=%d down_batches=%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.upBatches.Load(), debug.downBatches.Load(), debug.upBytes.Load(), debug.downBytes.Load(), debug.dropped.Load(), debug.errors.Load()) } }() } diff --git a/core/internal/protocol/vpnproto.go b/core/internal/protocol/vpnproto.go index 6331bf0..377de9f 100644 --- a/core/internal/protocol/vpnproto.go +++ b/core/internal/protocol/vpnproto.go @@ -20,8 +20,17 @@ const ( VPNRespClosed byte = 0x44 VPNRespError byte = 0x7f - VPNNoAck uint32 = 0xffffffff - VPNMaxFragment = 65535 + VPNNoAck uint32 = 0xffffffff + + // Raw IP packets remain bounded by the IPv4/IPv6 packet-length model. + VPNMaxPacket = 65535 + + // DragonTCP transfer objects/records are independent of IP packet size. + // Multiple IP packets may be batched into one transfer object. + VPNMaxFragment = 1024 * 1024 + VPNMaxBatch = 1024 * 1024 + + VPNBatchVersion byte = 1 ) type VPNSessionID [16]byte @@ -56,7 +65,7 @@ func BuildVPNOpen(sid VPNSessionID, token string, ipv4, ipv6 netip.Addr, mtu int if !ipv4.Is4() || !ipv6.Is6() { return nil, errors.New("invalid VPN client addresses") } - if mtu < 576 || mtu > 65535 { + if mtu < 576 || mtu > VPNMaxPacket { return nil, errors.New("invalid VPN MTU") } out := make([]byte, 1+16+2+len(token)+4+16+2) @@ -84,7 +93,7 @@ func ParseVPNOpen(payload []byte) (sid VPNSessionID, token string, ipv4, ipv6 ne 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 { + if len(payload) != need { err = errors.New("bad VPN OPEN length") return } @@ -103,6 +112,8 @@ func ParseVPNOpen(payload []byte) (sid VPNSessionID, token string, ipv4, ipv6 ne return } +// OPENED v2 response: cmd(1) maxChunk(4). +// ParseVPNOpened also accepts the old 3-byte/uint16 response for compatibility. func BuildVPNOpened(maxChunk int) []byte { if maxChunk > VPNMaxFragment { maxChunk = VPNMaxFragment @@ -110,9 +121,9 @@ func BuildVPNOpened(maxChunk int) []byte { if maxChunk < 1 { maxChunk = 1 } - out := make([]byte, 3) + out := make([]byte, 5) out[0] = VPNRespOpened - binary.BigEndian.PutUint16(out[1:3], uint16(maxChunk)) + binary.BigEndian.PutUint32(out[1:5], uint32(maxChunk)) return out } @@ -120,48 +131,55 @@ 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") + if len(payload) == 5 && payload[0] == VPNRespOpened { + v := int(binary.BigEndian.Uint32(payload[1:5])) + if v < 1 || v > VPNMaxFragment { + return 0, errors.New("bad VPN OPENED max chunk") + } + return v, nil } - return int(binary.BigEndian.Uint16(payload[1:3])), nil + if len(payload) == 3 && payload[0] == VPNRespOpened { + return int(binary.BigEndian.Uint16(payload[1:3])), nil + } + return 0, errors.New("bad VPN OPENED response") } -// PUSH request: cmd(1) sid(16) seq(4) offset(2) total(2) data(N) +// PUSH v2 request: cmd(1) sid(16) seq(4) offset(4) total(4) 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 { + if total < 1 || total > VPNMaxBatch || 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 := make([]byte, 29+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) + binary.BigEndian.PutUint32(out[21:25], uint32(offset)) + binary.BigEndian.PutUint32(out[25:29], uint32(total)) + copy(out[29:], 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 { + if len(payload) < 30 || 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 { + offset = int(binary.BigEndian.Uint32(payload[21:25])) + total = int(binary.BigEndian.Uint32(payload[25:29])) + data = payload[29:] + if total < 1 || total > VPNMaxBatch || offset < 0 || offset > total || len(data) < 1 || len(data) > VPNMaxFragment || 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 := make([]byte, 9) out[0] = VPNRespAck binary.BigEndian.PutUint32(out[1:5], seq) - binary.BigEndian.PutUint16(out[5:7], uint16(accepted)) + binary.BigEndian.PutUint32(out[5:9], uint32(accepted)) return out } @@ -170,54 +188,54 @@ func ParseVPNAck(payload []byte) (seq uint32, accepted int, err error) { err = e return } - if len(payload) != 7 || payload[0] != VPNRespAck { + if len(payload) != 9 || 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])) + accepted = int(binary.BigEndian.Uint32(payload[5:9])) return } -// PULL request: cmd(1) sid(16) ack(4) want(4) offset(2) limit(2) +// PULL v2 request: cmd(1) sid(16) ack(4) want(4) offset(4) limit(4) func BuildVPNPull(sid VPNSessionID, ack, want uint32, offset, limit int) ([]byte, error) { - if offset < 0 || offset > 65535 || limit < 1 || limit > VPNMaxFragment { + if offset < 0 || offset > VPNMaxBatch || limit < 1 || limit > VPNMaxFragment { return nil, errors.New("invalid VPN PULL") } - out := make([]byte, 29) + out := make([]byte, 33) 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)) + binary.BigEndian.PutUint32(out[25:29], uint32(offset)) + binary.BigEndian.PutUint32(out[29:33], uint32(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 { + if len(payload) != 33 || 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") + offset = int(binary.BigEndian.Uint32(payload[25:29])) + limit = int(binary.BigEndian.Uint32(payload[29:33])) + if offset < 0 || offset > VPNMaxBatch || limit < 1 || limit > VPNMaxFragment { + err = errors.New("bad VPN PULL bounds") } return } -// DATA response: cmd(1) seq(4) offset(2) total(2) data(N) +// DATA v2 response: cmd(1) seq(4) offset(4) total(4) data(N) func BuildVPNData(seq uint32, offset, total int, data []byte) []byte { - out := make([]byte, 9+len(data)) + out := make([]byte, 13+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) + binary.BigEndian.PutUint32(out[5:9], uint32(offset)) + binary.BigEndian.PutUint32(out[9:13], uint32(total)) + copy(out[13:], data) return out } @@ -230,20 +248,111 @@ func ParseVPNData(payload []byte) (seq uint32, offset, total int, data []byte, w wait = true return } - if len(payload) < 10 || payload[0] != VPNRespData { + if len(payload) < 14 || 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 { + offset = int(binary.BigEndian.Uint32(payload[5:9])) + total = int(binary.BigEndian.Uint32(payload[9:13])) + data = payload[13:] + if total < 1 || total > VPNMaxBatch || offset < 0 || offset+len(data) > total || len(data) < 1 || len(data) > VPNMaxFragment { err = errors.New("bad VPN DATA bounds") } return } +// A transfer object is a batch of raw IP packets: +// version(1), then repeated packetLen(2) + packet bytes. +func BuildVPNBatch(packets [][]byte) ([]byte, error) { + if len(packets) == 0 { + return nil, errors.New("empty VPN batch") + } + total := 1 + for _, packet := range packets { + if len(packet) < 1 || len(packet) > VPNMaxPacket { + return nil, errors.New("invalid IP packet length in VPN batch") + } + total += 2 + len(packet) + if total > VPNMaxBatch { + return nil, errors.New("VPN batch exceeds maximum") + } + } + out := make([]byte, total) + out[0] = VPNBatchVersion + pos := 1 + for _, packet := range packets { + binary.BigEndian.PutUint16(out[pos:pos+2], uint16(len(packet))) + pos += 2 + copy(out[pos:pos+len(packet)], packet) + pos += len(packet) + } + return out, nil +} + +func ParseVPNBatch(batch []byte) ([][]byte, error) { + if len(batch) < 4 || len(batch) > VPNMaxBatch || batch[0] != VPNBatchVersion { + return nil, errors.New("bad VPN batch") + } + packets := make([][]byte, 0, 8) + pos := 1 + for pos < len(batch) { + if pos+2 > len(batch) { + return nil, errors.New("truncated VPN batch packet length") + } + n := int(binary.BigEndian.Uint16(batch[pos : pos+2])) + pos += 2 + if n < 1 || n > VPNMaxPacket || pos+n > len(batch) { + return nil, errors.New("invalid VPN batch packet") + } + packet := make([]byte, n) + copy(packet, batch[pos:pos+n]) + packets = append(packets, packet) + pos += n + } + if len(packets) == 0 { + return nil, errors.New("VPN batch contains no packets") + } + return packets, nil +} + +// PacketAddresses returns the source and destination addresses from a raw +// IPv4/IPv6 packet. The packet may contain trailing bytes; the IP header's own +// length field is validated against the supplied buffer. +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 BuildVPNClose(sid VPNSessionID) []byte { out := make([]byte, 17) out[0] = VPNCmdClose