Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kcptun-rs ⚡

Rust port of kcptun — up to 5.38× faster than Go, fully wire-compatible

Build Status Tests E2E License Rust Go Compatible

English | 中文


Disclaimer — This project is a Vibe Coding porting test, for educational purposes only.

This project is a Vibe Coding experiment — using AI-assisted programming to port an existing codebase. The core focus is exploring and validating the Vibe Coding workflow itself, not producing a production-grade software port. This is not production software. No guarantees are made regarding correctness, stability, or security.

It is strictly prohibited for any illegal use, including but not limited to circumventing censorship, illegal data transmission, network attacks, etc. Any illegal activities by users are not related to this project or its author. Users bear all legal responsibilities.

See DISCLAIMER.md for the full disclaimer.


🔥 Performance at a Glance

kcptun-rs outperforms Go kcptun across nearly every cipher and compression setting, while maintaining full wire compatibility — meaning you can replace one side of a Go kcptun tunnel with the Rust binary and get an instant speed boost.

Cipher vs Go (Tokio) vs Go (Smol)
SM4 (nocomp) 4.76× faster 🏆 4.58× faster
SM4 (comp) 5.32× faster 🏆 5.38× faster
XOR (nocomp) 2.54× faster 2.34× faster
CAST5 (nocomp) 1.98× faster 1.78× faster
Twofish (comp) 1.69× faster 1.22× faster
AES-128 (nocomp) 1.59× faster 1.41× faster
AES-128-CFB bulk 1.67× faster 2.11× faster 🏆

Benchmarked on Apple M1, 10 concurrent connections × 1 MB each. Full matrix below.


📖 What is kcptun-rs?

kcptun is a stable & secure TCP-over-UDP tunnel that uses KCP (a fast ARQ protocol) to accelerate TCP streams over high-latency or lossy networks. It features SMUX multiplexing, Reed-Solomon FEC, Snappy compression, and optional encryption — all wrapped in a single binary.

kcptun-rs is a complete Rust reimplementation that is:

  • Wire-compatible with Go kcptun (kcp-go v5) — Rust ↔ Go, Go → Rust, Rust → Rust all work
  • Faster than Go on most cipher/mode combinations (up to 5.38×)
  • 🧩 13 encryption backends + AES-128-GCM: AES, SM4, Salsa20, Blowfish, Twofish, CAST5, 3DES, TEA, XTEA, XOR, and more
  • 🔧 Dual async runtimes: tokio (high-concurrency) and smol (lightweight, ARM)
  • 🎯 Production-ready features: FEC, SMUX v1/v2, QPP obfuscation, SNMP stats, rate limiting, pprof profiling
  • 🔄 Cross-platform: macOS, Linux, ARMv7 (Raspberry Pi), ARM64 (Graviton)

✨ Features

Category Details
Compatibility Full wire-compatible with Go kcptun (kcp-go v5) — all ciphers, modes, SMUX versions, FEC, Snappy
Encryption 14 backends: null, none, xor, aes-128, aes-192, aes(256), aes-128-gcm, sm4, tea, xtea, salsa20, blowfish, twofish, cast5, 3des
KCP Modes normal, fast, fast2, fast3
SMUX v1 & v2 multiplexing — multiple TCP streams over a single KCP connection
FEC Reed-Solomon forward error correction (Go-compatible 10/3 default)
Compression Session-level Snappy compression, Go byte-identical, on by default
QPP Quantum Permutation Pad — optional post-quantum stream obfuscation
Runtimes tokio (default, multi-threaded) or smol (lightweight, ARM-optimized)
Profiling Optional --pprof emits Go-compatible protobuf → go tool pprof
Rate Limiting Per-connection token bucket pacing (--ratelimit)
SNMP Stats Go-compatible SNMP fields with zero-cost opt-in collection
Cross-Compile ARMv7 (Raspberry Pi), ARM64 (Graviton), Linux musl — all from macOS
Logging Structured log levels (RUST_LOG), optional file logging

🚀 Quick Start

# Build (optimized release with LTO)
cargo build --release
# Binary: target/release/kcptun-server, target/release/kcptun-client

# Start server (listens on UDP :29900, forwards to local HTTP :8080)
./target/release/kcptun-server -t "127.0.0.1:8080" -l ":29900" --key "my-secret"

# Start client (listens on :12948, tunnels to remote server)
./target/release/kcptun-client -r "server-ip:29900" -l ":12948" --key "my-secret"

Now point your application at 127.0.0.1:12948 — all TCP data is encrypted, compressed, and accelerated over KCP to the remote server.

With config file

kcptun-server -c config.json
kcptun-client -c config.json
{
    "localaddr": ":12948",
    "remoteaddr": "vps:29900",
    "key": "my-secret",
    "crypt": "aes-128",
    "mode": "fast2",
    "conn": 2,
    "sndwnd": 1024,
    "rcvwnd": 1024,
    "datashard": 10,
    "parityshard": 3,
    "nocomp": false,
    "smuxver": 2,
    "keepalive": 10
}

⚠️ --key, --crypt, --mode, and --nocomp must match between server and client. Compression is enabled by default.


📊 Performance Deep Dive

Bulk Throughput (200 MB, AES-128-CFB, no compression)

Path labels are Client → Server (bulk data leaves the client toward the server; from bench/run_bench.sh).

Path (Client → Server) Throughput Latency vs Go→Go
Go → Go 51.15 MB/s 0.31 ms 1.00×
Rust-Tokio → Rust-Tokio 85.60 MB/s 🥈 0.12 ms 1.67×
Rust-Smol → Rust-Smol 108.06 MB/s 🏆 0.13 ms 2.11×
Rust-Tokio → Go 76.48 MB/s 0.11 ms 1.50×
Go → Rust-Tokio 30.28 MB/s 0.15 ms 0.59×

Same-stack Rust paths are clearly faster than Go→Go on this M1 host. The smol runtime's lightweight architecture gives it an edge in single-stream bulk transfer.

Full Cipher × Compression Matrix

Tests: 10 concurrent connections, 1 MB each, all 30+ runs per cell passed (0 failures).

Without compression (--nocomp):

Cipher Tokio Smol Go T/Go S/Go
null 38.4 38.8 35.5 1.08× 1.09×
none 29.4 33.3 39.2 0.75× 0.85×
xor 41.6 38.3 16.4 2.54× 2.34×
aes-128 43.4 38.3 27.2 1.59× 1.41×
aes-128-gcm 36.6 35.8 41.5 0.88× 0.86×
salsa20 35.8 35.4 32.3 1.11× 1.10×
blowfish 31.5 31.3 28.6 1.10× 1.09×
twofish 35.1 37.1 23.2 1.51× 1.60×
cast5 33.3 30.1 16.9 1.98× 1.78×
3des 14.5 12.6 11.8 1.23× 1.07×
tea 38.2 35.2 31.7 1.20× 1.11×
xtea 24.7 22.2 18.6 1.33× 1.20×
sm4 16.7 16.1 3.5 4.76× 🏆 4.58×

With compression (Snappy):

Cipher Tokio Smol Go T/Go S/Go
aes-128-gcm 36.4 36.0 27.4 1.33× 1.31×
salsa20 29.0 30.6 20.1 1.44× 1.52×
sm4 18.7 18.8 3.5 5.32× 🏆 5.38× 🏆
twofish 34.4 24.9 20.4 1.69× 1.22×
cast5 36.5 34.3 26.4 1.38× 1.30×
blowfish 34.4 26.3 25.7 1.33× 1.02×
aes-128 31.3 35.7 26.5 1.18× 1.35×

SM4 is the standout: Rust outperforms Go by 4.6–5.4× because the Go implementation uses a pure software S-box while Rust benefits from compiler auto-vectorization and pre-computed lookup tables.

Stress Tests (Data Integrity)

All 8 stress tests pass — verifying byte-for-byte accuracy under concurrent load:

Test Connections Payload Result
Single conn, mixed sizes 1 1B…64KB
Multi-thread 10 conn 10 256B each
Multi-thread 50 conn 50 255B each
Multi-thread 100 conn 100 1B + 4KB
Large data (100 conn) 100 64KB + 128KB
Page refresh simulation 80 (3 waves) 512B…128KB
Compressible data 1 patterns

🔗 Go Compatibility

kcptun-rs is fully wire-compatible with Go kcptun (kcp-go v5). All 68 end-to-end interop tests pass across every combination:

Feature Status Notes
KCP segment format 24-byte LE header, same as kcp-go v5
Crypto header (CFB) [nonce 16B][CRC32 4B][payload]
AES-GCM [nonce 12B][ciphertext+tag 16B]
Snappy (session-level) Byte-identical to Go's github.com/golang/snappy
SMUX v1 & v2 Full frame format compatibility
FEC (10/3, 4/2) Reed-Solomon, same header format
Key derivation PBKDF2-HMAC-SHA1, salt b"kcp-go"
QPP obfuscation Stream-level, same permutation algorithm
All 15 ciphers Bidirectional (Go→Rust, Rust→Go)
All 4 KCP modes normal, fast, fast2, fast3
SM4 (Chinese national standard) tjfoc/gmsm S-box + CK fix
CAST5 (RFC 2144) Full implementation, ported from Go

E2E Test Results

Encryption:  15/15 ciphers passed (Go→Rust + Rust→Go)
KCP Modes:   4/4 passed
SMUX:        2/2 versions passed
Compression: 8/8 cipher×compression combos passed
FEC:         2/2 configurations passed
Total:       68 passed, 0 failed, 0 skipped 🎉

🏗️ Architecture

Protocol Stack

┌──────────────────────────────────┐
│         TCP / UNIX Socket        │
├──────────────────────────────────┤
│        SMUX Stream (mux)         │
├──────────────────────────────────┤
│       SMUX Session (mux)         │
├──────────────────────────────────┤
│  Snappy Compression (session)    │  ← byte-identical to Go
├──────────────────────────────────┤
│  BlockCrypt / FEC / KCP (ARQ)    │
├──────────────────────────────────┤
│           UDP / TCPraw           │
└──────────────────────────────────┘

Workspace (9 crates)

kcptun-rs/
├── kcp-rs/          — KCP ARQ protocol state machine
├── kcrypt-rs/       — 13 block ciphers + AES-128-GCM
├── smux-rs/         — SMUX stream multiplexer (v1/v2)
├── qpp-rs/          — Quantum Permutation Pad obfuscation
├── kio-rs/          — Async runtime abstraction (tokio / smol)
├── kpprof-rs/       — Go-compatible pprof HTTP server
├── kcptun-common/   — Shared client/server helpers
├── kcptun-client/   — Client binary
└── kcptun-server/   — Server binary + stress tests

Dual Runtime Design

  • tokio (default) — multi-threaded, high-concurrency, production-scaled
  • smol (--no-default-features --features smol) — lightweight, minimal binary, ARM-optimized
  • Mutually exclusive features — pick one per build
  • Business code uses kio::* abstractions only — never raw tokio/smol APIs

Flush Loop Optimization

The flush loop is split into 4 phases to minimize KCP mutex hold time:

Phase Work KCP Lock
1 Drain SMUX send buffers, collect FIN-pending streams ❌ Not held
2 Encode SMUX frames ❌ Not held
3 Snappy compress (if enabled) ❌ Not held
4 kcp.send() + kcp.update() + kcp.flush() ✅ Held briefly

This allows the UDP recv loop to feed data into KCP while the flush loop prepares the next batch of frames — eliminating lock contention under high concurrency.


🔧 Build & Run

Makefile Targets

make build          # Debug build (tokio)
make release        # Release build (LTO, stripped, panic=abort)
make test           # All unit tests
make stress         # Data integrity stress tests (needs release first)
make e2e            # Go↔Rust interop (needs Go kcptun binaries)
make clippy         # Lint (warnings = errors)
make fmt            # Format all Rust code
make profile        # Flamegraph profiling (samply → Speedscope)

Cross-Compilation

make install-cross     # Install cross toolchains (one-time)
make release-armv7     # Raspberry Pi 2/3, OpenWrt (~1.3M binary)
make release-arm64     # Raspberry Pi 4/5, AWS Graviton
make linux             # x86_64 Linux musl (from macOS)
make linux-aarch64     # ARM64 Linux musl (from macOS)

ARM cross builds use the smol runtime with pprof disabled for minimal binary size.

Linux Binaries from macOS

Build fully static musl binaries directly from macOS — ideal for CI or deployment testing:

make linux              # x86_64 musl (smol, ~1.3M)
make linux-aarch64      # ARM64 musl (smol)
make linux-full         # x86_64 musl + QPP

OS-Level UDP Buffer Tuning (macOS)

On macOS, the default UDP socket buffer sizes are small (typically 256 KB for send, 256 KB for receive). Under high-throughput KCP workloads — especially with large windows (sndwnd/rcvwnd ≥ 512) or high concurrency — the kernel UDP receive buffer can overflow, causing silent packet drops that inflate P99/P999 tail latency and trigger KCP retransmit storms.

Increasing the kernel socket buffer limits eliminates this bottleneck. This is the single most effective OS-level tuning for P99/P999 latency on macOS:

# Raise max socket buffer to 8 MB (default ~256 KB)
sudo sysctl -w kern.ipc.maxsockbuf=8388608

# Raise UDP receive buffer to 4 MB (default ~256 KB)
sudo sysctl -w net.inet.udp.recvspace=4194304

Effect on P99/P999 (measured): With these settings applied, the raw KCP layer's max sustainable throughput on loopback improved from 2975 → 3802 req/s (tokio, +28%) and 2411 → 2921 req/s (Go, +21%), with P99 latency dropping from 14.2 ms → 10.5 ms (tokio) and 20.3 ms → 15.9 ms (Go). See bench/LATENCY_P99_REPORT.md for full numbers.

To make the change persistent across reboots, add to /etc/sysctl.conf:

kern.ipc.maxsockbuf=8388608
net.inet.udp.recvspace=4194304

Linux equivalent: net.core.rmem_max, net.core.rmem_default, net.core.wmem_max, net.core.wmem_default — set to 4194304 or higher. Some distributions also require net.core.netdev_max_backlog.


🔬 Optimization Journey

The project evolved from 5.4 MB/s to over 108 MB/s through evidence-driven profiling:

Milestone Throughput vs Go
Initial port 5.4 MB/s 0.71×
+ Event-driven flush scheduling 7.1 MB/s 0.87×
+ Zero-copy KCP output pipeline 68.8 MB/s 1.43×
+ ARMv8 AES hardware acceleration ~85 MB/s 1.67×
+ Tokio persistent blocking pool +108% 2.1×
+ SMUX v2 write window control performance unblocked
+ Snappy offload & threshold tuning
+ sendmmsg/recvmmsg batch I/O
+ Cipher enum static dispatch vtable eliminated
+ macOS UDP buffer tuning (sysctl) P99 −26%, throughput +28%
Final (smol bulk) 108 MB/s 2.11× 🏆

Notable Bug Fixes Found Along the Way

Bug Impact Fix
Blowfish key schedule per block 0.0 MB/s (100× improvement) Store cipher instance
Twofish key schedule per block 0.4 → 4.5 MB/s (11×) Custom pre-computed tables
CRC32C vs CRC32/IEEE in Snappy Data silently dropped by Go Switch to snap::FrameEncoder
KCP ACK never populated Infinite retransmission → deadlock Queue ACKs for every received Push
snd_buf never cleaned Window stuck at 32 packets Front-of-buffer cleanup in flush()
Twofish 256-bit key S-box Wrong ciphertext vs Go Added 5th sbox layer

p99 Latency Collapse Investigation (256KB @ High Concurrency)

Symptom: the raw kcp-rs KcpConn (no tunnel layers) collapsed on large payloads under sustained load — 256KB round-trip at RPS=300 went from ~4ms to p50=3.2s, while Go with the identical 512/512 window + Fast3 config stayed at 19ms. Single-request latency was already fast (4.3ms); the pipeline stalled only when requests overlapped.

Root cause: every received KCP segment triggered a full flush_with_current() (kcp.input → parse_una>0 → iterate the whole snd_buf ~500 segments for retransmit checks). At 50k+ pkt/s that is ~30M snd_buf iterations/s, inflating per-segment cost to ~680µs and driving a fast/early retransmit storm (~20K/2s).

Effective fixes (kept):

Fix Where Result (256KB@RPS=300)
Batch input flush: input_no_flush() + flush_if_pending() (one deferred flush per recv burst, single lock) kcp.rs, conn.rs 3183ms → 2.1ms, 100% ok (was 87%), retrans storm → 0
Flush loop trusts kcp.flush() return (clamp 1..10ms) instead of forcing 1ms conn.rs flush-loop churn ~5-10× lower
P3: nodelay window-probe interval 500→50ms (IKCP_PROBE_INIT_NODELAY) kcp.rs collapse-edge recovery 947ms → 47ms at RPS=250
Same batch-flush applied to the legacy binary sessions (input_no_flush + one flush_if_pending per datagram FEC group) kcptun-client, kcptun-server 256KB@RPS=300: p50 12.4→10.5ms (legacy tunnel doesn't collapse — 1024-window + FEC + SMUX buffering keep it below the cliff)
Gate fast/early retransmit on new_segs_count > 0 — only retransmit when the window can carry new data; a fastack on an in-flight segment under a full window is usually a DELAYED ACK, not loss kcp.rs RPS=300 p99 69ms→3.9ms; RPS≤450 clean (~2.3ms)
write_notifynotify_one() (permit-storing) — the old notify_waiters lost wakes that landed before the waiter registered, forcing 10ms fallbacks under load conn.rs RPS=475 clean 2.3ms (was 539ms collapse); RPS=500 p50 500ms+→~100ms

Tunnel comparison (why the raw lib's extreme-load queueing isn't a lib defect): the same kcp_rs::KcpConn sustains 256KB@RPS=500 at ~11ms, 100% ok when used the way the product uses it (the default shared-session tunnel, copy_bidirectional two-task per conn) — vs Go tunnel 30.5ms. The raw benchmark's residual RPS=500 deep-queue is the single-task serial-echo worst case at 131 MB/s on one connection; the tunnel's SMUX/TCP layers decouple read from write. macOS exposes no public sendmmsg/recvmmsg (libSystem has no symbols), so batch I/O there would require raw syscalls (not implemented).

Rust now sustains 300 RPS of 256KB at ~2.1ms — ~9× faster than Go's 19ms. Wire format unchanged; verified by Go↔Rust interop (both directions 500/500).

Ineffective approaches tested and reverted (recorded so they aren't re-attempted):

Approach Outcome
Asymmetric window (rcv_wnd=2048) No change — disproved wnd=0 deadlock as the primary cause (Go uses 512/512 too)
Suppress retransmit when rmt_wnd==0 No change — rmt_wnd stays >0 during the collapse
Disable fast/early retransmit entirely 4× worse — retransmits were recovering real packet loss
ackOnly input flush (skip snd_buf scan) 5× worse — the data-recovery half of flush is load-bearing
Drain-first recv loops (+ yield_now) deadlock — the reactor wait was the implicit yield
Listener-reader batch drain 3.7× worse

🧪 Testing Rigor

Test Type Count What It Verifies
Unit tests 237 Individual crate correctness
E2E interop 68 Go↔Rust bidirectional compatibility
Stress tests 8 Byte-for-byte data integrity at scale
Clippy -D warnings Zero warnings enforced
Fmt cargo fmt --check Consistent formatting

📋 CLI Options

kcptun-client

Flag Default Description
-l / --localaddr :12948 Local listening address
-r / --remoteaddr (required) KCP server address
--key it's a secrect Pre-shared secret
--crypt aes Encryption algorithm
--mode fast KCP mode
--conn 1 Number of UDP connections
--mtu 1350 Maximum transmission unit
--sndwnd 1024 Send window
--rcvwnd 1024 Receive window
--datashard 0 FEC data shards
--parityshard 0 FEC parity shards
--ratelimit 0 Rate limit (bytes/sec)
--nocomp false Disable Snappy compression
--smuxver 2 SMUX version (1 or 2)
--keepalive 10 Keepalive interval (sec)
--QPP false Enable QPP obfuscation

kcptun-server

Flag Default Description
-l / --listen :29900 KCP listen address
-t / --target (required) TCP target address
--key it's a secrect Pre-shared secret
--crypt aes Encryption (same as client)
--mode fast KCP mode

💡 Why Rust?

  • Memory safety — no dangling pointers, no buffer overflows, no use-after-free
  • Zero-cost abstractions — enum dispatch eliminates vtable overhead on hot crypto paths
  • True parallelismstd::thread::scope for parallel batch encryption, no GIL
  • Compile-time guarantees — the borrow checker catches data races before they happen
  • ARM ecosystem — Rust is a first-class citizen on aarch64, with hardware AES available (aes_armv8)
  • Small binaries — stripped release binary is ~2 MB, far smaller than Go's statically linked blobs
  • Cross-compilation — build ARM Linux binaries from macOS with a single make command

📝 License

MIT — see LICENSE for details.

This is a Rust port of kcptun by xtaci.
Source: github.com/xsean2020/kcptun-rs


If you find this project useful or impressive, please ⭐ star it on GitHub!

Built with Rust, driven by curiosity, validated by benchmarks.

About

kcptun-rs is a Vibe Coding porting test — an experiment in AI-assisted programming, using kcptun (a KCP-based TCP stream accelerator by xtaci) as the reference target.

Resources

Stars

25 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages