v3
This commit is contained in:
@@ -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
|
||||
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
|
||||
maximum fragment 65535 bytes
|
||||
poll wait 100ms
|
||||
queued packet limit 2048/client
|
||||
queued byte limit 8 MiB/client
|
||||
auto NAT enabled
|
||||
private targets blocked
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.dragontcp.client"
|
||||
android:versionCode="3"
|
||||
android:versionName="2.0">
|
||||
android:versionCode="4"
|
||||
android:versionName="3.0">
|
||||
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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;
|
||||
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{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,27 +466,40 @@ 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
|
||||
}
|
||||
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)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
@@ -404,12 +509,15 @@ func (v *vpnClient) downloadLoop(errs chan<- error) {
|
||||
errs <- io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
v.downPackets.Add(1)
|
||||
v.downBytes.Add(uint64(n))
|
||||
rawBytes += 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)
|
||||
|
||||
@@ -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
|
||||
@@ -75,14 +78,21 @@ type vpnSession struct {
|
||||
mtu int
|
||||
maxChunk int
|
||||
maxPackets int
|
||||
maxQueueBytes int
|
||||
batchDelay time.Duration
|
||||
manager *vpnManager
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
packets map[uint32][]byte
|
||||
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,6 +341,10 @@ 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()
|
||||
@@ -269,6 +357,8 @@ type vpnManager struct {
|
||||
byIPv6 map[netip.Addr]*vpnSession
|
||||
maxChunk int
|
||||
maxPackets int
|
||||
maxQueueBytes int
|
||||
batchDelay time.Duration
|
||||
pollWait time.Duration
|
||||
timeout time.Duration
|
||||
tun *os.File
|
||||
@@ -280,12 +370,12 @@ type vpnManager struct {
|
||||
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())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,16 @@ const (
|
||||
VPNRespError byte = 0x7f
|
||||
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user