This commit is contained in:
2026-08-16 02:50:24 -03:00
parent 6dac260155
commit 5621de243a
8 changed files with 628 additions and 353 deletions
+109 -152
View File
@@ -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 DragonTCP v3 is a real Android layer-3 VPN over DragonTCP's adaptive XOR-framed
VPN packet tunnel. 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 This release fixes two important problems from the previous packet-VPN build:
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 1. Android can emit IPv6 link-local/control packets such as `fe80::...` on the
raw IPv4/IPv6 packets through DragonTCP's adaptive, XOR-framed TCP/53 VPN TUN. Those packets no longer terminate the DragonTCP session. The client
transport. 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 ## Architecture
@@ -18,59 +26,58 @@ Android apps
v v
Android VpnService TUN (MTU 1280) Android VpnService TUN (MTU 1280)
| |
| raw IPv4/IPv6 packets
v 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 v
DragonTCP VPN server TCP/53
|
v
DragonTCP Linux server
| |
v v
Linux TUN dragontcp0 Linux TUN dragontcp0
| |
| IP forwarding + NAT | forwarding / NAT
v v
Internet Internet
``` ```
Because complete IP packets are tunneled, this carries TCP, UDP, DNS, ICMP, Because the tunnel carries raw IP packets, it can carry TCP, UDP, DNS, ICMP,
IPv4 and IPv6. Applications do not need HTTP or SOCKS proxy support. IPv4 and IPv6. It does not depend on applications supporting an HTTP proxy.
## Included files ## Included files
```text ```text
bin/dragontcp-vpn-server-linux-amd64 bin/dragontcp-vpn-server-linux-amd64
bin/dragontcp-vpn-server-linux-arm64 bin/dragontcp-vpn-server-linux-arm64
bin/dragontcp-vpn-client-linux-amd64 # test/debug client
android/build/DragonTCP-VPN.apk android/build/DragonTCP-VPN.apk
android/lib/arm64-v8a/libdragontcp_vpn.so android/lib/arm64-v8a/libdragontcp_vpn.so
core/ complete Go source core/ # complete Go source
android/src/ complete Android Java source android/src/ # complete Android Java source
build_core.sh build_core.sh
build_all.sh build_all.sh
android/build_apk.sh android/build_apk.sh
``` ```
## Server requirements ## Server
The full VPN server needs root/CAP_NET_ADMIN because it creates a Linux TUN The server needs root or equivalent CAP_NET_ADMIN permissions because it
interface and enables packet forwarding/NAT. 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 ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y iproute2 iptables sudo apt-get install -y iproute2 iptables
``` ```
TCP port 53 must be free. Start:
Check:
```bash
sudo ss -lntp | grep ':53'
```
## Start the server
```bash ```bash
sudo ./dragontcp-vpn-server-linux-amd64 \ sudo ./dragontcp-vpn-server-linux-amd64 \
@@ -78,130 +85,125 @@ sudo ./dragontcp-vpn-server-linux-amd64 \
--debug --debug
``` ```
The defaults are: Defaults:
```text ```text
listen 0.0.0.0:53/TCP listen TCP port 53
TUN dragontcp0 server chunk max 1048576 bytes (1 MiB)
transfer batch max 1048576 bytes (1 MiB)
batch delay 1ms
TUN MTU 1280 TUN MTU 1280
server IPv4 10.123.0.1/16 server IPv4 10.123.0.1/16
server IPv6 fd7a:4472:6167:6f6e::1/64 server IPv6 fd7a:4472:6167:6f6e::1/64
maximum fragment 65535 bytes
poll wait 100ms poll wait 100ms
queued packet limit 2048/client
queued byte limit 8 MiB/client
auto NAT enabled auto NAT enabled
private targets blocked
``` ```
The server automatically enables IPv4/IPv6 forwarding and installs Useful explicit command:
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:
```bash ```bash
sudo ./dragontcp-vpn-server-linux-amd64 \ sudo ./dragontcp-vpn-server-linux-amd64 \
--token 'YOUR_SECRET' \ --token 'YOUR_SECRET' \
--chunk-max 1048576 \
--vpn-buffer-bytes 8388608 \
--batch-delay 1ms \
--debug \ --debug \
--debug-stats-interval 5s --debug-stats-interval 5s
``` ```
Very verbose per-IP-packet diagnostics: Per-packet/batch diagnostics are very verbose:
```bash ```bash
--debug-packets --debug-packets
``` ```
Do not leave `--debug-packets` enabled for high-throughput use.
## Android app ## Android app
Install: Install `android/build/DragonTCP-VPN.apk`.
```text The UI asks for:
DragonTCP-VPN.apk
```
The UI is intentionally small:
```text ```text
Server Server
TCP Port TCP port
Token Token
Maximum fragment Maximum transport fragment
Minimum fragment Minimum transport fragment
Timeout Timeout
CONNECT
STOP
Live log
``` ```
Defaults: Defaults:
```text ```text
Port 53 Port 53
Max 1280 Max 1048576
Min 32 Min 32
Timeout 2s Timeout 2s
Pollers 1 (fixed) Pollers 1 (fixed)
MTU 1280 (fixed) VPN MTU 1280
``` ```
The starting DragonTCP record size is always the configured maximum. On a The adaptive record starts at Max and shrinks after transport failures. The
transport failure the client automatically reduces it. With Max=1280 and 1 MiB value is a DragonTCP transport ceiling, not the IP MTU.
Min=32 the reduction path can converge approximately as:
### 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 ```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 ## Android link-local source fix
run. The DragonTCP app UID itself is excluded from the VPN so the TCP/53
transport cannot recursively enter its own TUN interface.
## 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 ```text
MTU is 1280, so an individual IP packet normally cannot exceed 1280 bytes. VPN stopped: source fe80::... does not match session address
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.
## 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: Requirements:
- Go 1.22+ - Go 1.22+
- JDK 17+ - JDK 17+
- Android SDK platform and build-tools - Android SDK platform/build-tools
- `zip` - 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 ```bash
export ANDROID_SDK_ROOT="$HOME/Android/Sdk" export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
``` ```
Build server, Android native core, and APK: Build native components and APK:
```bash ```bash
./build_all.sh ./build_all.sh
@@ -216,76 +218,31 @@ android/lib/arm64-v8a/libdragontcp_vpn.so
android/build/DragonTCP-VPN.apk android/build/DragonTCP-VPN.apk
``` ```
Build only Go/native components: Build only the Go/native components:
```bash ```bash
./build_core.sh ./build_core.sh
``` ```
Build only APK after the core is present: Build only the APK after the native core exists:
```bash ```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()`. The Go packages compile with `go test ./...`.
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 A local mock-TUN test verified:
`ProcessBuilder`.
## 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: A physical Android phone is still required to validate device/vendor-specific
`VpnService` behavior and the real mobile-network TCP/53 path.
```text
request : UP + request-id + length + XOR(payload)
response : OK + request-id + length + XOR(payload)
```
The VPN payload protocol is binary rather than text to reduce overhead on very
small records.
Commands include:
```text
VOPEN
VPUSH fragment
VPULL fragment
VCLOSE
```
A random 128-bit session ID is used after authenticated session creation.
Packets and fragments have sequence/offset fields so retries do not duplicate
bytes.
## Test mode
For protocol testing without root/TUN/NAT, the server has:
```bash
./dragontcp-vpn-server-linux-amd64 \
--host 127.0.0.1 \
--port 19053 \
--token test \
--mock-echo
```
This echoes complete IP packets back to the client instead of forwarding them
to the Internet.
During development the packet path was tested with IPv4 and IPv6 1280-byte
packets while the server forced a 32-byte maximum DragonTCP fragment. Both
were reassembled byte-for-byte correctly.
## Security
XOR 0xAD remains protocol obfuscation, not cryptographic encryption. HTTPS
and other TLS-based application protocols retain their own end-to-end
security, but the DragonTCP transport itself should not be considered
cryptographically confidential.
+4 -4
View File
@@ -1,4 +1,4 @@
1c854f81ee4d493c7e7b5956f7a81d08a4019e6a1ba9aef00ca64df702cdc90f android/build/DragonTCP-VPN.apk 26b46523f9100ebb7c84a606cfad37d5d17e45a1c6eb1571340f7b5f74668f3e /mnt/data/DragonTCP-VPN-v3/android/build/DragonTCP-VPN.apk
a05c475d98b922c053142cd2f65b52c22facd8404370108671010846c26019bc bin/dragontcp-vpn-server-linux-amd64 2e70feec6ea544efc9a3b3ea8c66a205c4827176d80374fa2b4b48845ec09e2e /mnt/data/DragonTCP-VPN-v3/bin/dragontcp-vpn-server-linux-amd64
29d52797045da9d13114264187bfe7445f78f3ef4fac225d29aec9348efb1be1 bin/dragontcp-vpn-server-linux-arm64 c049c8449b01d46049c2cf4a0368d23562219025d38aa9024ea3966c15dcc3b0 /mnt/data/DragonTCP-VPN-v3/bin/dragontcp-vpn-server-linux-arm64
5d8282fd03af2178a3061711edba177ab144db50a0aeaa170d53598ff795fb07 android/lib/arm64-v8a/libdragontcp_vpn.so c0f600c1cff7bc7809c2afab0272fe528d36a7cac6815820e8fd454e81b0894c /mnt/data/DragonTCP-VPN-v3/android/lib/arm64-v8a/libdragontcp_vpn.so
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dragontcp.client" package="com.dragontcp.client"
android:versionCode="3" android:versionCode="4"
android:versionName="2.0"> android:versionName="3.0">
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -43,7 +43,7 @@ public class DragonService extends VpnService {
if(!ACTION_CONNECT.equals(action))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")); 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"); 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;} 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; start=max;
try{ try{
@@ -96,13 +96,13 @@ public class MainActivity extends Activity {
setContentView(page); 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 int intValue(EditText e,int d){try{return Integer.parseInt(e.getText().toString().trim());}catch(Exception x){return d;}}
private boolean validateSettings(){ private boolean validateSettings(){
if(server.getText().toString().trim().isEmpty()){toast("Enter the server IP or hostname");return false;} 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(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;} if(timeout.getText().toString().trim().isEmpty()){toast("Enter a timeout such as 2s");return false;}
return true; 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 String clientIPv6(int id){return "fd7a:4472:6167:6f6e::"+Integer.toHexString(id);}
private Intent buildServiceIntent(){ 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("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("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; i.putExtra("vpnIPv4",clientIPv4(id));i.putExtra("vpnIPv6",clientIPv6(id));return i;
+141 -30
View File
@@ -238,20 +238,25 @@ type vpnClient struct {
ipv4, ipv6 netip.Addr ipv4, ipv6 netip.Addr
mtu int mtu int
timeout time.Duration timeout time.Duration
batchDelay time.Duration
reconnectEvery int reconnectEvery int
upSizer, downSizer *adaptiveSizer upSizer, downSizer *adaptiveSizer
control, upload, download *txnLane control, upload, download *txnLane
upPackets, downPackets, upBytes, downBytes atomic.Uint64 upPackets, downPackets, upBytes, downBytes atomic.Uint64
upBatches, downBatches, localDropped atomic.Uint64
stopped chan struct{} stopped chan struct{}
stopOnce sync.Once 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() sid, err := randomSID()
if err != nil { if err != nil {
return nil, err 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), 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 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) { func (v *vpnClient) logLocalDrop(reason string) {
buf := make([]byte, 65535) n := v.localDropped.Add(1)
var seq uint32 // 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 { for {
n, err := v.tun.Read(buf) n, err := v.tun.Read(buf)
if err != nil { if err != nil {
errs <- err errs <- err
return return
} }
if n < 1 { if n < 1 || n > protocol.VPNMaxPacket {
continue continue
} }
packet := append([]byte(nil), buf[:n]...) packet := append([]byte(nil), buf[:n]...)
if n > 65535 { src, _, err := protocol.PacketAddresses(packet)
if err != nil {
v.logLocalDrop(err.Error())
continue 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 offset := 0
for offset < n { for offset < len(batch) {
limit := v.upSizer.Current() limit := v.upSizer.Current()
size := n - offset size := len(batch) - offset
if size > limit { if size > limit {
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 { if e != nil {
errs <- e errs <- e
return return
@@ -334,16 +422,20 @@ func (v *vpnClient) uploadLoop(errs chan<- error) {
errs <- e errs <- e
return return
} }
if rseq != seq || accepted < offset || accepted > n { if rseq != seq || accepted < offset || accepted > len(batch) {
errs <- errors.New("bad server upload ACK") errs <- errors.New("bad server upload ACK")
return return
} }
fullRecord := size == limit v.upSizer.Success(size, size == limit)
v.upSizer.Success(size, fullRecord)
offset = accepted offset = accepted
} }
v.upPackets.Add(1) var rawBytes uint64
v.upBytes.Add(uint64(n)) for _, p := range packets {
rawBytes += uint64(len(p))
}
v.upPackets.Add(uint64(len(packets)))
v.upBytes.Add(rawBytes)
v.upBatches.Add(1)
seq++ seq++
} }
} }
@@ -352,7 +444,7 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
var want uint32 var want uint32
ack := protocol.VPNNoAck ack := protocol.VPNNoAck
offset := 0 offset := 0
var packet []byte var transfer []byte
total := 0 total := 0
for { for {
limit := v.downSizer.Current() limit := v.downSizer.Current()
@@ -374,27 +466,40 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
if wait { if wait {
continue 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") errs <- errors.New("bad server download sequence")
return return
} }
if offset == 0 { if offset == 0 {
total = rtotal total = rtotal
packet = make([]byte, 0, total) transfer = make([]byte, 0, total)
} else if rtotal != total { } else if rtotal != total {
errs <- errors.New("download packet size changed") errs <- errors.New("download transfer size changed")
return return
} }
packet = append(packet, data...) transfer = append(transfer, data...)
offset += len(data) offset += len(data)
v.downSizer.Success(len(data), len(data) == limit) v.downSizer.Success(len(data), len(data) == limit)
if offset < total { if offset < total {
continue continue
} }
if offset != total { if offset != total {
errs <- errors.New("download packet overflow") errs <- errors.New("download transfer overflow")
return return
} }
packets, e := protocol.ParseVPNBatch(transfer)
if e != nil {
// 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
}
}
var rawBytes uint64
for _, packet := range packets {
n, e := v.tun.Write(packet) n, e := v.tun.Write(packet)
if e != nil { if e != nil {
errs <- e errs <- e
@@ -404,12 +509,15 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
errs <- io.ErrShortWrite errs <- io.ErrShortWrite
return return
} }
v.downPackets.Add(1) rawBytes += uint64(n)
v.downBytes.Add(uint64(n)) }
v.downPackets.Add(uint64(len(packets)))
v.downBytes.Add(rawBytes)
v.downBatches.Add(1)
ack = want ack = want
want++ want++
offset = 0 offset = 0
packet = nil transfer = nil
total = 0 total = 0
} }
} }
@@ -419,8 +527,10 @@ func (v *vpnClient) run() error {
return err return err
} }
fmt.Println("VPN READY") fmt.Println("VPN READY")
errs := make(chan error, 2) errs := make(chan error, 3)
go v.uploadLoop(errs) packets := make(chan []byte, 256)
go v.tunReadLoop(packets, errs)
go v.uploadLoop(packets, errs)
go v.downloadLoop(errs) go v.downloadLoop(errs)
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() defer ticker.Stop()
@@ -429,7 +539,7 @@ func (v *vpnClient) run() error {
case err := <-errs: case err := <-errs:
return err return err
case <-ticker.C: 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: case <-v.stopped:
return nil return nil
} }
@@ -445,11 +555,12 @@ func main() {
ipv4Text := flag.String("vpn-ipv4", "10.123.0.2", "client VPN IPv4 address") 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") ipv6Text := flag.String("vpn-ipv6", "fd7a:4472:6167:6f6e::2", "client VPN IPv6 address")
mtu := flag.Int("vpn-mtu", 1280, "VPN interface MTU") 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") 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") 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") 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") 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") adaptLog := flag.Bool("chunk-adapt-log", false, "log adaptive chunk changes")
flag.Parse() flag.Parse()
@@ -496,7 +607,7 @@ func main() {
} }
} }
addr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort)) 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 { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
+176 -78
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"crypto/subtle" "crypto/subtle"
"encoding/hex" "encoding/hex"
"errors" "errors"
@@ -36,6 +37,8 @@ type debugStats struct {
activeSessions atomic.Int64 activeSessions atomic.Int64
upPackets atomic.Uint64 upPackets atomic.Uint64
downPackets atomic.Uint64 downPackets atomic.Uint64
upBatches atomic.Uint64
downBatches atomic.Uint64
upBytes atomic.Uint64 upBytes atomic.Uint64
downBytes atomic.Uint64 downBytes atomic.Uint64
dropped atomic.Uint64 dropped atomic.Uint64
@@ -75,14 +78,21 @@ type vpnSession struct {
mtu int mtu int
maxChunk int maxChunk int
maxPackets int maxPackets int
maxQueueBytes int
batchDelay time.Duration
manager *vpnManager manager *vpnManager
mu sync.Mutex mu sync.Mutex
notify chan struct{} notify chan struct{}
packets map[uint32][]byte packets map[uint32]*downTransfer
nextDown uint32 nextDown uint32
closed bool closed bool
lastSeen time.Time lastSeen time.Time
pendingPackets [][]byte
pendingEncoded int
pendingTimer *time.Timer
queuedPacketCount int
queuedBytes int
upMu sync.Mutex upMu sync.Mutex
expectedUp uint32 expectedUp uint32
@@ -95,10 +105,16 @@ type vpnSession struct {
haveLastComplete bool 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{ return &vpnSession{
sid: sid, ipv4: v4, ipv6: v6, mtu: mtu, maxChunk: maxChunk, maxPackets: maxPackets, 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][]byte, maxPackets), lastSeen: time.Now(), 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) touchLocked() { s.lastSeen = time.Now() }
func (s *vpnSession) touch() { s.mu.Lock(); s.touchLocked(); s.mu.Unlock() } 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 { func (s *vpnSession) enqueue(packet []byte) bool {
if len(packet) == 0 || len(packet) > 65535 { if len(packet) == 0 || len(packet) > protocol.VPNMaxPacket {
return false return false
} }
s.mu.Lock() s.mu.Lock()
@@ -118,21 +180,39 @@ func (s *vpnSession) enqueue(packet []byte) bool {
if s.closed { if s.closed {
return false 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 { if s.manager.debug != nil {
s.manager.debug.dropped.Add(1) s.manager.debug.dropped.Add(1)
} }
return false return false
} }
seq := s.nextDown p := append([]byte(nil), packet...)
s.nextDown++ if len(s.pendingPackets) == 0 {
s.packets[seq] = append([]byte(nil), packet...) s.pendingEncoded = 1
}
s.pendingPackets = append(s.pendingPackets, p)
s.pendingEncoded += 2 + len(p)
s.queuedPacketCount++
s.queuedBytes += len(p)
s.touchLocked() s.touchLocked()
s.signalLocked()
if s.manager.debug != nil { if s.manager.debug != nil {
s.manager.debug.downPackets.Add(1) s.manager.debug.downPackets.Add(1)
s.manager.debug.downBytes.Add(uint64(len(packet))) 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 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) { func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, error) {
s.upMu.Lock() s.upMu.Lock()
defer s.upMu.Unlock() defer s.upMu.Unlock()
if total < 1 || total > 65535 || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total { if total < 1 || total > protocol.VPNMaxBatch || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total {
return 0, errors.New("invalid packet fragment") return 0, errors.New("invalid transfer fragment")
} }
if s.haveLastComplete && seq == s.lastComplete { 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) s.currentBuf = make([]byte, 0, total)
} }
if s.currentSeq != seq || s.currentTotal != 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 // Idempotent retry: if this exact offset was already accepted, acknowledge
// the existing bytes instead of appending duplicate data. // the existing bytes instead of appending duplicate data.
if offset < len(s.currentBuf) { if offset < len(s.currentBuf) {
end := offset + len(data) 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 len(s.currentBuf), nil
} }
return 0, errors.New("retry fragment does not match accepted data") 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 return accepted, nil
} }
packet := append([]byte(nil), s.currentBuf...) transfer := append([]byte(nil), s.currentBuf...)
s.haveCurrent = false s.haveCurrent = false
s.currentBuf = nil s.currentBuf = nil
if err := s.manager.acceptClientPacket(s, packet); err != nil { if err := s.manager.acceptClientTransfer(s, transfer); err != nil {
return 0, err return 0, err
} }
@@ -202,9 +282,8 @@ func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, erro
s.expectedUp++ s.expectedUp++
s.touch() s.touch()
if s.manager.debug != nil { if s.manager.debug != nil {
s.manager.debug.upPackets.Add(1) s.manager.debug.upBatches.Add(1)
s.manager.debug.upBytes.Add(uint64(len(packet))) s.manager.debug.packetf("UP BATCH sid=%s seq=%d transfer_bytes=%d", shortSID(s.sid), seq, len(transfer))
s.manager.debug.packetf("UP sid=%s seq=%d bytes=%d", shortSID(s.sid), seq, len(packet))
} }
return accepted, nil return accepted, nil
} }
@@ -221,21 +300,26 @@ func (s *vpnSession) pull(ack, want uint32, offset, limit int, wait time.Duratio
if ack != protocol.VPNNoAck { if ack != protocol.VPNNoAck {
for seq := range s.packets { for seq := range s.packets {
if seq <= ack { if seq <= ack {
rec := s.packets[seq]
if rec != nil {
s.queuedPacketCount -= rec.packetCount
s.queuedBytes -= rec.rawBytes
}
delete(s.packets, seq) delete(s.packets, seq)
} }
} }
} }
if packet, ok := s.packets[want]; ok { if rec, ok := s.packets[want]; ok {
if offset >= len(packet) { if offset >= len(rec.data) {
s.mu.Unlock() 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 end := offset + limit
if end > len(packet) { if end > len(rec.data) {
end = len(packet) end = len(rec.data)
} }
out := append([]byte(nil), packet[offset:end]...) out := append([]byte(nil), rec.data[offset:end]...)
total := len(packet) total := len(rec.data)
s.mu.Unlock() s.mu.Unlock()
return out, total, false, nil return out, total, false, nil
} }
@@ -257,6 +341,10 @@ func (s *vpnSession) close() {
s.mu.Lock() s.mu.Lock()
if !s.closed { if !s.closed {
s.closed = true s.closed = true
if s.pendingTimer != nil {
s.pendingTimer.Stop()
s.pendingTimer = nil
}
s.signalLocked() s.signalLocked()
} }
s.mu.Unlock() s.mu.Unlock()
@@ -269,6 +357,8 @@ type vpnManager struct {
byIPv6 map[netip.Addr]*vpnSession byIPv6 map[netip.Addr]*vpnSession
maxChunk int maxChunk int
maxPackets int maxPackets int
maxQueueBytes int
batchDelay time.Duration
pollWait time.Duration pollWait time.Duration
timeout time.Duration timeout time.Duration
tun *os.File tun *os.File
@@ -280,12 +370,12 @@ type vpnManager struct {
v6Prefix 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) v4p := netip.MustParsePrefix(defaultVPNv4Prefix)
v6p := netip.MustParsePrefix(defaultVPNv6Prefix) v6p := netip.MustParsePrefix(defaultVPNv6Prefix)
m := &vpnManager{ m := &vpnManager{
sessions: make(map[protocol.VPNSessionID]*vpnSession), byIPv4: make(map[netip.Addr]*vpnSession), byIPv6: make(map[netip.Addr]*vpnSession), 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, v4Prefix: v4p, v6Prefix: v6p,
} }
if tun != nil { 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 { if m.byIPv4[v4] != nil || m.byIPv6[v6] != nil {
return nil, errors.New("client VPN address already in use") 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.sessions[sid] = s
m.byIPv4[v4] = s m.byIPv4[v4] = s
m.byIPv6[v6] = 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 { func destinationAllowed(dst netip.Addr, allowPrivate bool) bool {
if dst.IsUnspecified() || dst.IsMulticast() { if dst.IsUnspecified() || dst.IsMulticast() {
return false return false
@@ -423,32 +479,68 @@ func destinationAllowed(dst netip.Addr, allowPrivate bool) bool {
return true return true
} }
func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) error { func (m *vpnManager) dropClientPacket(s *vpnSession, packet []byte, reason string) {
src, dst, err := packetAddresses(packet) 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 { if err != nil {
return err m.dropClientPacket(s, packet, err.Error())
return false, nil
} }
if src != s.ipv4 && src != s.ipv6 { 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) { 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 { if m.mockEcho {
s.enqueue(packet) s.enqueue(packet)
return nil return true, nil
} }
if m.tun == nil { if m.tun == nil {
return errors.New("VPN TUN is unavailable") return false, errors.New("VPN TUN is unavailable")
} }
m.tunWriteMu.Lock() m.tunWriteMu.Lock()
n, err := m.tun.Write(packet) n, err := m.tun.Write(packet)
m.tunWriteMu.Unlock() m.tunWriteMu.Unlock()
if err != nil { if err != nil {
return err return false, err
} }
if n != len(packet) { 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 return nil
} }
@@ -467,7 +559,7 @@ func (m *vpnManager) tunReadLoop() {
continue continue
} }
packet := append([]byte(nil), buf[:n]...) packet := append([]byte(nil), buf[:n]...)
_, dst, e := packetAddresses(packet) _, dst, e := protocol.PacketAddresses(packet)
if e != nil { if e != nil {
continue continue
} }
@@ -667,8 +759,10 @@ func main() {
port := flag.Int("port", 53, "listen TCP port") port := flag.Int("port", 53, "listen TCP port")
token := flag.String("token", "change-this-token", "shared token") token := flag.String("token", "change-this-token", "shared token")
maxConnections := flag.Int("max-connections", 20000, "maximum simultaneous TCP/53 connections") 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") 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") 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") sessionTimeout := flag.Duration("session-timeout", 5*time.Minute, "idle VPN session timeout")
tunName := flag.String("tun", "dragontcp0", "Linux TUN interface name") 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) fmt.Fprintf(os.Stderr, "--chunk-max must be 32-%d\n", protocol.VPNMaxFragment)
os.Exit(2) 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 { if *mtu < 576 || *mtu > 9000 {
fmt.Fprintln(os.Stderr, "--mtu must be 576-9000") fmt.Fprintln(os.Stderr, "--mtu must be 576-9000")
os.Exit(2) os.Exit(2)
@@ -699,7 +797,7 @@ func main() {
} }
defer tun.Close() 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)) addr := net.JoinHostPort(*host, strconv.Itoa(*port))
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
@@ -713,13 +811,13 @@ func main() {
} else { } 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("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 { if debug.enabled && *statsEvery > 0 {
go func() { go func() {
t := time.NewTicker(*statsEvery) t := time.NewTicker(*statsEvery)
defer t.Stop() defer t.Stop()
for range t.C { 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())
} }
}() }()
} }
+151 -42
View File
@@ -21,7 +21,16 @@ const (
VPNRespError byte = 0x7f VPNRespError byte = 0x7f
VPNNoAck uint32 = 0xffffffff VPNNoAck uint32 = 0xffffffff
VPNMaxFragment = 65535
// 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 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() { if !ipv4.Is4() || !ipv6.Is6() {
return nil, errors.New("invalid VPN client addresses") 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") return nil, errors.New("invalid VPN MTU")
} }
out := make([]byte, 1+16+2+len(token)+4+16+2) 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]) copy(sid[:], payload[1:17])
tokenLen := int(binary.BigEndian.Uint16(payload[17:19])) tokenLen := int(binary.BigEndian.Uint16(payload[17:19]))
need := 1 + 16 + 2 + tokenLen + 4 + 16 + 2 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") err = errors.New("bad VPN OPEN length")
return return
} }
@@ -103,6 +112,8 @@ func ParseVPNOpen(payload []byte) (sid VPNSessionID, token string, ipv4, ipv6 ne
return return
} }
// OPENED v2 response: cmd(1) maxChunk(4).
// ParseVPNOpened also accepts the old 3-byte/uint16 response for compatibility.
func BuildVPNOpened(maxChunk int) []byte { func BuildVPNOpened(maxChunk int) []byte {
if maxChunk > VPNMaxFragment { if maxChunk > VPNMaxFragment {
maxChunk = VPNMaxFragment maxChunk = VPNMaxFragment
@@ -110,9 +121,9 @@ func BuildVPNOpened(maxChunk int) []byte {
if maxChunk < 1 { if maxChunk < 1 {
maxChunk = 1 maxChunk = 1
} }
out := make([]byte, 3) out := make([]byte, 5)
out[0] = VPNRespOpened out[0] = VPNRespOpened
binary.BigEndian.PutUint16(out[1:3], uint16(maxChunk)) binary.BigEndian.PutUint32(out[1:5], uint32(maxChunk))
return out return out
} }
@@ -120,48 +131,55 @@ func ParseVPNOpened(payload []byte) (int, error) {
if err := ParseVPNError(payload); err != nil { if err := ParseVPNError(payload); err != nil {
return 0, err return 0, err
} }
if len(payload) != 3 || payload[0] != VPNRespOpened { if len(payload) == 5 && payload[0] == VPNRespOpened {
return 0, errors.New("bad VPN OPENED response") 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
}
if len(payload) == 3 && payload[0] == VPNRespOpened {
return int(binary.BigEndian.Uint16(payload[1:3])), nil 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) { 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") return nil, errors.New("invalid VPN PUSH fragment")
} }
out := make([]byte, 25+len(data)) out := make([]byte, 29+len(data))
out[0] = VPNCmdPush out[0] = VPNCmdPush
copy(out[1:17], sid[:]) copy(out[1:17], sid[:])
binary.BigEndian.PutUint32(out[17:21], seq) binary.BigEndian.PutUint32(out[17:21], seq)
binary.BigEndian.PutUint16(out[21:23], uint16(offset)) binary.BigEndian.PutUint32(out[21:25], uint32(offset))
binary.BigEndian.PutUint16(out[23:25], uint16(total)) binary.BigEndian.PutUint32(out[25:29], uint32(total))
copy(out[25:], data) copy(out[29:], data)
return out, nil return out, nil
} }
func ParseVPNPush(payload []byte) (sid VPNSessionID, seq uint32, offset, total int, data []byte, err error) { 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") err = errors.New("bad VPN PUSH")
return return
} }
copy(sid[:], payload[1:17]) copy(sid[:], payload[1:17])
seq = binary.BigEndian.Uint32(payload[17:21]) seq = binary.BigEndian.Uint32(payload[17:21])
offset = int(binary.BigEndian.Uint16(payload[21:23])) offset = int(binary.BigEndian.Uint32(payload[21:25]))
total = int(binary.BigEndian.Uint16(payload[23:25])) total = int(binary.BigEndian.Uint32(payload[25:29]))
data = payload[25:] data = payload[29:]
if total < 1 || offset < 0 || offset > total || len(data) < 1 || offset+len(data) > total { 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") err = errors.New("bad VPN PUSH fragment bounds")
} }
return return
} }
func BuildVPNAck(seq uint32, accepted int) []byte { func BuildVPNAck(seq uint32, accepted int) []byte {
out := make([]byte, 7) out := make([]byte, 9)
out[0] = VPNRespAck out[0] = VPNRespAck
binary.BigEndian.PutUint32(out[1:5], seq) 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 return out
} }
@@ -170,54 +188,54 @@ func ParseVPNAck(payload []byte) (seq uint32, accepted int, err error) {
err = e err = e
return return
} }
if len(payload) != 7 || payload[0] != VPNRespAck { if len(payload) != 9 || payload[0] != VPNRespAck {
err = errors.New("bad VPN ACK") err = errors.New("bad VPN ACK")
return return
} }
seq = binary.BigEndian.Uint32(payload[1:5]) seq = binary.BigEndian.Uint32(payload[1:5])
accepted = int(binary.BigEndian.Uint16(payload[5:7])) accepted = int(binary.BigEndian.Uint32(payload[5:9]))
return 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) { 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") return nil, errors.New("invalid VPN PULL")
} }
out := make([]byte, 29) out := make([]byte, 33)
out[0] = VPNCmdPull out[0] = VPNCmdPull
copy(out[1:17], sid[:]) copy(out[1:17], sid[:])
binary.BigEndian.PutUint32(out[17:21], ack) binary.BigEndian.PutUint32(out[17:21], ack)
binary.BigEndian.PutUint32(out[21:25], want) binary.BigEndian.PutUint32(out[21:25], want)
binary.BigEndian.PutUint16(out[25:27], uint16(offset)) binary.BigEndian.PutUint32(out[25:29], uint32(offset))
binary.BigEndian.PutUint16(out[27:29], uint16(limit)) binary.BigEndian.PutUint32(out[29:33], uint32(limit))
return out, nil return out, nil
} }
func ParseVPNPull(payload []byte) (sid VPNSessionID, ack, want uint32, offset, limit int, err error) { 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") err = errors.New("bad VPN PULL")
return return
} }
copy(sid[:], payload[1:17]) copy(sid[:], payload[1:17])
ack = binary.BigEndian.Uint32(payload[17:21]) ack = binary.BigEndian.Uint32(payload[17:21])
want = binary.BigEndian.Uint32(payload[21:25]) want = binary.BigEndian.Uint32(payload[21:25])
offset = int(binary.BigEndian.Uint16(payload[25:27])) offset = int(binary.BigEndian.Uint32(payload[25:29]))
limit = int(binary.BigEndian.Uint16(payload[27:29])) limit = int(binary.BigEndian.Uint32(payload[29:33]))
if limit < 1 { if offset < 0 || offset > VPNMaxBatch || limit < 1 || limit > VPNMaxFragment {
err = errors.New("bad VPN PULL limit") err = errors.New("bad VPN PULL bounds")
} }
return 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 { 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 out[0] = VPNRespData
binary.BigEndian.PutUint32(out[1:5], seq) binary.BigEndian.PutUint32(out[1:5], seq)
binary.BigEndian.PutUint16(out[5:7], uint16(offset)) binary.BigEndian.PutUint32(out[5:9], uint32(offset))
binary.BigEndian.PutUint16(out[7:9], uint16(total)) binary.BigEndian.PutUint32(out[9:13], uint32(total))
copy(out[9:], data) copy(out[13:], data)
return out return out
} }
@@ -230,20 +248,111 @@ func ParseVPNData(payload []byte) (seq uint32, offset, total int, data []byte, w
wait = true wait = true
return return
} }
if len(payload) < 10 || payload[0] != VPNRespData { if len(payload) < 14 || payload[0] != VPNRespData {
err = fmt.Errorf("bad VPN DATA response type/length") err = fmt.Errorf("bad VPN DATA response type/length")
return return
} }
seq = binary.BigEndian.Uint32(payload[1:5]) seq = binary.BigEndian.Uint32(payload[1:5])
offset = int(binary.BigEndian.Uint16(payload[5:7])) offset = int(binary.BigEndian.Uint32(payload[5:9]))
total = int(binary.BigEndian.Uint16(payload[7:9])) total = int(binary.BigEndian.Uint32(payload[9:13]))
data = payload[9:] data = payload[13:]
if total < 1 || offset < 0 || offset+len(data) > total || len(data) < 1 { if total < 1 || total > VPNMaxBatch || offset < 0 || offset+len(data) > total || len(data) < 1 || len(data) > VPNMaxFragment {
err = errors.New("bad VPN DATA bounds") err = errors.New("bad VPN DATA bounds")
} }
return 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 { func BuildVPNClose(sid VPNSessionID) []byte {
out := make([]byte, 17) out := make([]byte, 17)
out[0] = VPNCmdClose out[0] = VPNCmdClose