Skip to content

PoemaIX/udp-obfus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

transparent-udp-obfus

Transparent, per-packet UDP payload obfuscator for Linux. Captures locally- originated UDP, XORs the payload against a passphrase-derived ChaCha20 keystream, and forwards it over the wire. The receiver decodes symmetrically and re-injects the cleartext UDP into the local stack, so applications see a normal UDP socket on both ends — no per-app changes, no userspace proxy, no MTU loss.

Intended for environments where a UDP tunnel (VXLAN, WireGuard behind a tunnel, QUIC, plain application UDP, …) should look like opaque UDP to on-path observers.

Features

  • Transparent: applications keep their sockets; no proxy in the hot path.
  • MTU-preserving: the payload length is unchanged.
  • Works with both user apps and kernel UDP tunnels (tested with VXLAN).
  • IPv4 and IPv6.
  • Three send modes: raw socket (raw), TUN write-back (tun_writeback), or UDP-socket pool (udp_socket); auto-detected when omitted.
  • Optional dst-port rewriting so the wire port differs from the app port.
  • Rule engine matches by AF, src/dst IP, src port range, dst port.
  • Clean RAII setup: all iptables / ip-rule / sysctl changes are rolled back on exit.

How it works

            ┌─ local UDP ─┐                            ┌─ local UDP ─┐
   app ───▶│  iif lo     │──▶ udpak0 (TUN) ──▶ encode ─│  raw send   │──▶ wire
            └─────────────┘                   XOR      └─────────────┘
                                                                         │
            ┌─── TUN  ────┐                   ┌── TPROXY mangle ──┐      │
   app ◀──│ write back  │◀── decode XOR ◀──│  dport match       │◀─────┘
            └─────────────┘                   └───────────────────┘

Sender side:

  1. An ip rule iif lo dport X fwmark 0/0x3 lookup <table> redirects locally-originated UDP matching local_traffic into a routing table whose only route is default dev udpak0.
  2. The obfus reads the packet from the TUN, parses the UDP header, XORs the payload with the keystream, optionally rewrites the dst port, recomputes the UDP checksum, and emits the packet.
  3. In raw mode the packet is re-sent via a raw socket (IP_HDRINCL / v6 HDRINCL) with SO_MARK = mark_emit. The mark bit prevents the capture rule from re-matching, so no loop.
  4. In tun_writeback mode the packet is written back into the TUN; the kernel then forwards it via the normal output path. This mode needs ip_forward=1 (saved and restored by the setup guard).

Receiver side:

  1. An iptables mangle PREROUTING rule matches UDP on the wire port and TPROXY-redirects it to the obfus listener, which runs with IP_TRANSPARENT + IP_RECVORIGDSTADDR.
  2. The obfus recovers the original 5-tuple from the ORIGDSTADDR cmsg, decodes the XOR, optionally rewrites the dst port back, and writes the cleartext packet into the TUN.
  3. A secondary routing table (table + 1) containing local 0.0.0.0/0 dev lo makes the kernel treat any dst IP as local for traffic coming from the TUN, so the decoded packet is delivered to the app (or to the kernel UDP-tunnel socket, e.g. VXLAN).

An ! -i <tun> match on the TPROXY rule keeps decoded packets from being re-captured when dstport_rewrite is not set.

Quick start

Build:

cargo build --release
sudo setcap cap_net_admin,cap_net_raw=eip target/release/transparent-udp-obfus
# or run as root

Minimal config (obfus.toml):

tun           = "udpak0"
table         = 100
tproxy_port   = 9000
mark_reinject = 0x3
mark_emit     = 0x2
mark_tproxy   = 0x1
passphrase    = "correct horse battery staple"
num_threads   = 2
sender_mode   = "raw"         # or "tun_writeback"

# Capture locally-originated UDP destined to port 4789 and ship it out
# as UDP/9887 on the wire.
[[local_traffic]]
af              = "ip4"
dstport         = 4789
dstport_rewrite = 9887

# On the remote side, receive UDP/9887, decode, deliver as UDP/4789.
[[remote_traffic]]
af              = "ip4"
dstport         = 9887
dstport_rewrite = 4789

Run (must be root for raw sockets / iptables / ip rule):

sudo ./transparent-udp-obfus --config obfus.toml

Both ends must share the same passphrase. All Linux state (TUN, ip rule, route tables, iptables TPROXY rule, rp_filter/ip_forward sysctls) is added at startup and rolled back on SIGINT / SIGTERM.

Configuration reference

Key Type Notes
tun string Interface name (≤15 chars). Created multi-queue.
table u32 Primary routing table for capture. table+1 is used for the tproxy local route.
tproxy_port u16 Local port for the transparent listener.
mark_reinject u32 (mask) fwmark mask that must be zero on the capture rule (fwmark 0/mask).
mark_emit u32 SO_MARK applied to raw-socket sends and tun_writeback sends; should overlap with mark_reinject so re-emitted packets skip the capture rule.
mark_tproxy u32 Mark the TPROXY rule sets; used to route to table+1 (local 0/0 dev lo). Default 0x1.
passphrase string Hashed with BLAKE3 to seed the ChaCha20 keystream.
num_threads usize Tokio worker threads and TUN queues. Defaults to available parallelism.
sender_mode enum raw, tun_writeback, or udp_socket. Omit to auto-detect: udp_socket if every local rule has srcport_rewrite with a range disjoint from srcport, else raw.
socket_idle_secs u64 Idle TTL for entries in the udp_socket pool. Default 10.
iptables_comment string Tag added to every iptables TPROXY rule (-m comment --comment X). Used by pre/post-clean to wipe leftovers from a crashed prior run. Default udp-obfus-<passphrase with whitespace stripped>.
cipher_mode enum chained (default) or plain. See below.
local_traffic array[rule] Match on locally-originated UDP to capture.
remote_traffic array[rule] Match on remote UDP to TPROXY-intercept.

Each rule:

Key Type Notes
af "ip4" | "ip6" Required.
srcip CIDR string Optional.
dstip CIDR string Optional.
srcport u16 or [lo, hi] Optional.
dstport u16 Optional.
dstport_rewrite u16 Optional. If set, overwrite dst port after encoding / before decoding.
srcport_rewrite u16 or [lo, hi] Optional. Requires srcport. Either a range of the same size as srcport (1:1 offset mapping) or a single port (N:1 flatten — every matched src port is squashed onto it). Sender maps app→wire; receiver maps back. Flatten mode is intended for symmetric NAT on the path: the NAT randomises the wire src port, so the receiver sets srcport_rewrite to a single port to pin everything back to a fixed app-side port.

Cipher mode

  • chained (default): tail→head chained XOR, C[i] = P[i] ^ K[i] ^ C[i+1]. Any byte change propagates through the head of the packet, so identical plaintext segments produce different ciphertext. Not SIMD-vectorizable because of the loop-carried dependency, so it runs scalar at ~1 byte/cycle.
  • plain: pure stateless buf[i] ^= K[i]. The compiler auto-vectorizes it to AVX2/NEON (10–30× faster in the inner loop), but identical plaintext at the same offset produces identical ciphertext. Useful only when XOR is the bottleneck — typically 10 Gbps+ or jumbo frames; for 1 Gbps-class VXLAN overlays the two modes are within noise of each other because syscalls and the UDP checksum dominate per-packet cost.

Both peers must use the same cipher_mode.

Operational notes

  • Must run as root, or with CAP_NET_ADMIN + CAP_NET_RAW.
  • rp_filter is set to 0 on both all and <tun> for the lifetime of the program (effective rp_filter is max(all, iface)). Previous values are restored on exit.
  • tun_writeback mode additionally enables ip_forward and accept_local on the TUN. accept_local is required because without it fib_validate_source rejects our own local src as a martian, independent of rp_filter.
  • Multi-queue TUN: the writer fd reuses sender_fds[0]. A dedicated writer fd would cause the kernel to hash some outgoing packets into a queue that nothing reads, silently dropping whole flows.
  • Matcher is first-match-wins. Put more specific rules before broader ones.

Tests

A netns-based integration test lives under tests/netns/. It builds a topology alice ↔ home_a ↔ isp ↔ home_b ↔ bob (two double-NATed peers with an ISP in between), runs obfus on both ends, and verifies:

  • cleartext roundtrip with both a user app (socat) and a kernel VXLAN tunnel,
  • that the ISP-side capture contains no cleartext marker,
  • all combinations of {ip4, ip6} × {raw, tun_writeback} × {rewrite on/off} × {userapp, vxlan} — 16 cases.

Run:

sudo bash tests/netns/run.sh

License

See LICENSE if present.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors