Skip to content

Xdp#43

Closed
ParsaKSH wants to merge 26 commits into
devfrom
xdp
Closed

Xdp#43
ParsaKSH wants to merge 26 commits into
devfrom
xdp

Conversation

@ParsaKSH

Copy link
Copy Markdown
Owner

No description provided.

ParsaKSH and others added 26 commits April 7, 2026 03:49
feat: implement asymmetric syn_udp transport and add multi-inbound su…
set workflows for panel
chore: update Zig installation path to user home directory in CI work…
refactor: migrate to pure Go SQLite driver to enable static cross-com…
New Core architecture, v3.0.0
fix: ensure tester results exist and optimize rendering by memoizing …
feat: replace download link and apply action with a client-side file …
feat: add support for dynamic web path prefixing to all frontend rout…
Added a donation section to the README file.
back runner to ubuntu22
feat: add file path support for IP lists, implement tunnel auto-start and update version to 3.0.2
feat: add TCP destination port filtering support for receiver mode
…ceive

## Architecture

Restructure Rust workspace into three crates:
- spoof-transport: existing raw socket transport (moved from rust/src/)
- xdp-ebpf: eBPF program compiled for bpfel-unknown-none target
- xdp-loader: userspace loader using aya for XDP program management

## XDP eBPF Program (rust/xdp-ebpf/)

- Parses Ethernet/IPv4 headers using fixed constant offsets (14+20)
  to satisfy the BPF verifier's strict bounds checking
- Filters packets by protocol, peer spoof IP, and destination port
  via BPF HashMap config map
- Uses bpf_xdp_load_bytes() kernel helper for payload copy into
  ring buffer, avoiding the verifier's variable-offset packet
  pointer access restrictions
- Inline asm (asm_experimental_arch) to emit AND/OR instructions
  that the BPF verifier can track for argument bounds
- Returns XDP_DROP for matched packets (zero-copy to userspace)
- Returns XDP_PASS for non-matching traffic (normal stack)

## XDP Loader (rust/xdp-loader/)

- Embeds compiled eBPF ELF at compile time via include_bytes!()
- build.rs tracks eBPF binary for proper change detection
- Aligned copy mechanism for ELF data (8-byte alignment required)
- Configures BPF maps: CONFIG (filter params), PAYLOADS (ring buf),
  PKT_COUNT (statistics)
- Ring buffer consumer thread delivers packets to Go via FFI
- Graceful XDP detach on close

## Go Integration

- New XDPReceiver type in internal/transport/xdp_receiver.go
- Factory pattern: --xdp-interface flag triggers XDP-first with
  automatic fallback to raw socket on failure
- CGo FFI via spoof_transport.h for XDP lifecycle management
- Config support: xdp_interface field in JSON config

## Build System

- xdp-ebpf excluded from Cargo workspace (separate nightly toolchain)
- Makefile updated for two-phase Rust build (eBPF then workspace)
- .gitignore updated for xdp-ebpf build artifacts

## Key BPF Verifier Solutions

- Fixed header offsets (no variable IHL/doff) for packet access
- bpf_xdp_load_bytes() instead of manual byte-copy loop
- Inline asm for payload_len masking (LLVM optimizes away Rust masks)
- Ring buffer entry sized to accommodate masked max (2064 bytes)
Add build-ebpf job to both test.yml and release.yml:
- Uses nightly Rust toolchain with rust-src component
- Compiles xdp-ebpf crate for bpfel-unknown-none target
- Passes eBPF binary to downstream jobs via upload-artifact

Update Rust build jobs:
- Install libelf-dev (required by aya for ELF object parsing)
- Download eBPF binary artifact before building Rust workspace
- Use -p spoof-transport flag in test workflow for targeted build

Job dependency graph:
  build-ebpf → test-rust
  build-ebpf → test-go     (test.yml)
  build-ebpf → test-rust → build-tunnel (release.yml)

The eBPF program is architecture-independent (bpfel-unknown-none)
so it only needs to be built once and shared across amd64/arm64 jobs.
…ection

## Root cause: | 1 bug causing ~50% packet loss

The previous asm block used 'r0 |= 1' to ensure the verifier saw
min=1. This corrupted the actual value:
  - payload_len=100 (even) → safe_len=101 → bpf_xdp_load_bytes
    tried to read 101 bytes from a 100-byte payload → FAIL
  - Failed entries were discarded → ~50% of packets dropped
  - Surviving packets had length off-by-one → garbled data

## Fix: single asm block with inline conditional branch

Use a single asm block that:
  1. Masks: r2 = payload_len & 0x7FF  (verifier: [0, 2047])
  2. Branch: if r2 == 0 goto skip     (verifier narrowing)
  3. Sets args and calls bpf_xdp_load_bytes#189 directly
  4. On zero path: returns -1 to signal skip

The verifier can follow the conditional branch and knows that on
the fall-through path r2 (the length arg) is in [1, 2047].
This completely avoids the 'zero-sized read' verifier error.

The zero case never triggers at runtime since payload_len >= 1
is checked earlier, but the verifier needs the proof.

## Also removes unused import
bpf_xdp_load_bytes is now called via raw asm 'call 189' so
the aya_ebpf_bindings import is no longer needed.
The asm block was not setting r2 (offset argument) correctly.
r2 contained the masked payload_len instead of payload_offset,
causing bpf_xdp_load_bytes to read from the wrong location in
the packet (e.g. offset 10 in IP header instead of offset 54
where TCP payload starts).

This resulted in IP/TCP header bytes being returned as payload
data, causing garbled output on the receiving end.

Fix: add payload_offset as input register r8 and set r2 = r8
before the call. Also fix the goto offset from +5 to +6 since
the call setup now uses 4 instructions instead of 3.

Args to bpf_xdp_load_bytes (helper #189):
  r1 = xdp_md context
  r2 = offset in packet      ← was wrong (payload_len), now correct
  r3 = destination buffer
  r4 = length to copy
When the TCP sender fragments packets (payload + TCP overhead > MTU),
the XDP program was DROPping the first fragment (which has the TCP
header) while PASSing subsequent fragments (no TCP header → bounds
check fail). The kernel couldn't reassemble because the first
fragment was missing, causing 0 throughput at higher WireGuard MTUs.

Fix: check IP flags/fragment-offset field (bytes 6-7 of IP header).
If MF (More Fragments) bit is set OR fragment offset > 0, return
XDP_PASS to let the kernel handle reassembly via the raw socket
receiver fallback.

This restores correct behavior for WireGuard MTU 1280+ while
keeping the XDP fast path for non-fragmented packets.
@ParsaKSH ParsaKSH closed this May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant