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.
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.
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)
| 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 |
# 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.
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--nocompmust match between server and client. Compression is enabled by default.
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.
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.
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 | ✅ |
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 |
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 🎉
┌──────────────────────────────────┐
│ TCP / UNIX Socket │
├──────────────────────────────────┤
│ SMUX Stream (mux) │
├──────────────────────────────────┤
│ SMUX Session (mux) │
├──────────────────────────────────┤
│ Snappy Compression (session) │ ← byte-identical to Go
├──────────────────────────────────┤
│ BlockCrypt / FEC / KCP (ARQ) │
├──────────────────────────────────┤
│ UDP / TCPraw │
└──────────────────────────────────┘
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
- 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
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.
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)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.
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 + QPPOn 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=4194304Effect 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 to4194304or higher. Some distributions also requirenet.core.netdev_max_backlog.
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× 🏆 |
| 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 |
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_notify → notify_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 |
| 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 |
| 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 |
| 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 |
- 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 parallelism —
std::thread::scopefor 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
makecommand
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.