A wire-compatible Rust rewrite of udp2raw. A UDP tunnel that encapsulates UDP traffic into encrypted FakeTCP/UDP/ICMP packets using raw sockets.
udp2raw-rs disguises UDP packets as TCP, UDP, or ICMP traffic to bypass firewalls and QoS rules that block or throttle plain UDP. It uses raw sockets to craft packets at the IP layer, with encryption and authentication to protect tunnel traffic.
This is a byte-for-byte compatible reimplementation of the original C++ udp2raw-tunnel. A Rust client can talk to a C++ server, and vice versa. Every byte on the wire is identical.
- FakeTCP/UDP/ICMP Encapsulation: Tunnel UDP through protocol headers that firewalls typically allow
- Wire-Compatible: Byte-identical packets with the C++ version, full interop guaranteed
- Encryption: AES-128-CBC, AES-128-CFB, XOR, or no cipher
- Authentication: HMAC-SHA1 (Encrypt-then-MAC), MD5, CRC32, simple hash, or none
- Anti-Replay: Sliding window (4000 packets) to reject duplicate/replayed packets
- Auto iptables: Automatically manages firewall rules to prevent kernel RST interference
- Lower-Level Mode: Send at OSI Layer 2 via
AF_PACKETfor environments without raw socket IP access - AF_XDP Transport: Optional high-performance kernel-bypass I/O via AF_XDP (compile with
--features xdp) - GRO Fix: Handle NIC Generic Receive Offload that coalesces packets
- No async runtime: Synchronous
mioevent loop withlibcFFI. No tokio, no threads for I/O
flowchart LR
A["Local App"] -->|UDP| B["udp2raw Client"]
B -->|"Encrypted FakeTCP/UDP/ICMP"| C["udp2raw Server"]
C -->|UDP| D["Remote App"]
D -->|UDP| C
C -->|"Encrypted FakeTCP/UDP/ICMP"| B
B -->|UDP| A
- Local application sends UDP to the tunnel client
- Client encrypts the payload, wraps it in a FakeTCP/UDP/ICMP packet, sends over raw socket
- Server receives the raw packet, decrypts it, forwards plain UDP to the target application
- Return traffic follows the reverse path
git clone https://github.com/brianpht/udp2raw-rs.git
cd udp2raw-rs
cargo build --release
# Binary at target/release/udp2raw
# With AF_XDP support (Linux >= 5.7):
cargo build --release --features xdp- Linux (raw sockets via
libcFFI) - Root or
CAP_NET_RAWcapability - Rust 1.70+
Server (on the remote machine):
sudo ./udp2raw -s -l 0.0.0.0:4096 -r 127.0.0.1:7777 -k "my_password" --raw-mode faketcp -aClient (on the local machine):
sudo ./udp2raw -c -l 127.0.0.1:3333 -r SERVER_IP:4096 -k "my_password" --raw-mode faketcp -aNow point your application at 127.0.0.1:3333. UDP traffic is tunneled through the encrypted FakeTCP connection to 127.0.0.1:7777 on the server.
udp2raw: UDP tunnel over encrypted raw sockets (FakeTCP/UDP/ICMP)
Options:
-c, --client Run as client
-s, --server Run as server
-l <ip:port> Local listen address
-r <ip:port> Remote address
-k, --key <password> Encryption password [default: "secret key"]
--raw-mode <mode> faketcp | udp | icmp [default: faketcp]
--cipher-mode <mode> aes128cbc | aes128cfb | xor | none [default: aes128cbc]
--auth-mode <mode> hmac_sha1 | md5 | crc32 | simple | none [default: hmac_sha1]
-a, --auto-rule Auto add/delete iptables rules
-g, --gen-rule Print iptables rule and exit
--log-level <0-6> 0=off, 4=info, 6=trace [default: 4]
--seq-mode <0-4> TCP sequence number mode [default: 3]
--lower-level <spec> Layer 2 mode: "if_name#dest_mac"
--fix-gro Fix GRO coalesced packets
--disable-anti-replay Disable anti-replay protection
--sock-buf <KB> Socket buffer size [default: 1024]
--conf-file <path> Load options from config file
--fifo <path> FIFO for runtime commands (e.g. "reconnect")
--xdp Use AF_XDP transport (requires --features xdp)
--xdp-queue <N> AF_XDP NIC queue ID [default: 0]
--xdp-dst-mac <mac> AF_XDP destination MAC (aa:bb:cc:dd:ee:ff)
--xdp-if <name> AF_XDP interface name (defaults to --dev)
Every packet on the wire is byte-identical to the C++ implementation. The protocol uses two packet formats:
Bare packet (handshake phase):
[iv:8B][padding:8B][marker='b':1B][data:NB] → encrypt
Safer packet (data/heartbeat phase):
[my_id:4B][opp_id:4B][seq:8B][type:1B][roller:1B][data:NB] → encrypt
Packet types: b'h' = heartbeat, b'd' = data, b'b' = bare.
HMAC mode (Encrypt-then-MAC):
encrypt: data → cipher_encrypt() → auth_cal() → output
decrypt: data → auth_verify() → cipher_decrypt() → output
Legacy mode (MAC-then-Encrypt):
encrypt: data → auth_cal() → cipher_encrypt() → output
decrypt: data → cipher_decrypt() → auth_verify() → output
password → MD5 → normal_key
password → PBKDF2-SHA256 (10000 rounds) → HKDF-SHA256 → {
"cipher_key client-->server"
"cipher_key server-->client"
"hmac_key client-->server"
"hmac_key server-->client"
}
Client encrypts with client→server key, decrypts with server→client key.
Server does the reverse. The HKDF info strings are wire-critical.
src/
├── main.rs Entry point, CLI dispatch, signal handlers
├── lib.rs Module re-exports for tests and examples
├── misc.rs CLI parsing (clap), Config struct, iptables management
├── common.rs Type aliases, constants, checksum, byte-order helpers
├── encrypt.rs AES-CBC/CFB, XOR, HMAC-SHA1, MD5, CRC32, PBKDF2, HKDF
├── connection.rs Wire protocol (send_bare, send_safer), anti-replay, conv manager
├── network.rs Raw socket I/O, #[repr(C,packed)] IP/TCP/UDP/ICMP headers, BPF
├── transport.rs RawTransport enum — dispatches to raw socket or AF_XDP backend
├── xdp.rs AF_XDP (kernel-bypass) transport (feature-gated: --features xdp)
├── client.rs Client event loop (mio::Poll + raw fd + UDP fd + timerfd)
├── server.rs Server event loop (mio::Poll + raw fd + per-conn UDP fds)
├── fd_manager.rs Fd64 abstraction to avoid OS fd-reuse collisions
├── mio_fd.rs Shared MioFdSource wrapper for mio::Poll registration
└── logging.rs Custom logger with timestamps and color
- Wire Compatibility > Correctness > Simplicity > Performance: if it doesn't produce byte-identical packets to the C++ version, it's broken
- No global mutable state:
Configis&Configeverywhere, mutable state owned by the event loop - No async runtime: synchronous
mio::Pollevent loop withSourceFdwrappers around raw fds - Minimal unsafe: only for
libc::FFI calls,#[repr(C, packed)]struct casting, andtimerfdintegration - No wrapper crates for raw socket I/O: direct
libc::socket,libc::sendto,libc::recvfrom
| Mode | Description | iptables rule |
|---|---|---|
faketcp |
Fake TCP with SYN/ACK handshake (default, best firewall compat) | blocks kernel RST |
udp |
Raw UDP encapsulation | blocks kernel ICMP port-unreachable |
icmp |
ICMP Echo request/reply encapsulation | blocks kernel ICMP reply |
| Mode | Description |
|---|---|
aes128cbc |
AES-128-CBC with custom padding (default) |
aes128cfb |
AES-128-CFB streaming cipher |
xor |
XOR with derived key (fast, weak) |
none |
No encryption |
| Mode | Description |
|---|---|
hmac_sha1 |
HMAC-SHA1 Encrypt-then-MAC (default, strongest) |
md5 |
MD5 hash |
crc32 |
CRC32 checksum |
simple |
Simple hash |
none |
No authentication (disables anti-replay) |
# /etc/udp2raw.conf
-c
-l 127.0.0.1:3333
-r server:4096
-k my_password
--raw-mode faketcp
-asudo ./udp2raw --conf-file /etc/udp2raw.conf# Run the examples
cargo run --example encrypt_decrypt # All cipher×auth encryption roundtrips
cargo run --example key_derivation # Inspect derived key material
cargo run --example packet_builder # Build and hex-dump raw packet headerscargo test # All tests (no root needed)
cargo test --test wire_protocol # Byte-layout verification
cargo test --test encrypt_cross # All cipher×auth roundtrips (client↔server)
cargo test --test packet_headers # Struct size/offset assertions
cargo test --test anti_replay # Sliding window correctnessAll tests verify wire compatibility with the C++ implementation.
| C++ udp2raw | udp2raw-rs | |
|---|---|---|
| Wire format | ✅ reference | ✅ byte-identical |
| Handshake | ✅ | ✅ compatible |
| Encryption | OpenSSL | RustCrypto (aes, cbc, hmac, sha2) |
| Event loop | libev | mio |
| Raw sockets | direct | libc FFI (direct) |
| Memory safety | manual | Rust ownership |
| Packet headers | C bitfields | #[repr(C, packed)] + bitwise ops |
| Binary size | ~300 KB | ~500 KB (static, stripped) |
This is a Rust reimplementation of udp2raw-tunnel by wangyu.