A ptrace-based syscall interceptor that transparently redirects UDP socket I/O through AF_XDP sockets — no source changes required in the target binary.
ggvisor forks the target binary under ptrace and intercepts every socket-related syscall at the kernel boundary:
- Entry interception — ggvisor replaces the syscall with an invalid no-op so the kernel does nothing.
- Synthetic return — ggvisor performs the equivalent operation itself (via AF_XDP) and writes the return value back into the tracee's registers.
- Transparent I/O — the target binary receives a normal fd and return codes; it never knows its traffic was redirected.
Intercepted syscalls: socket, bind, connect, sendto, sendmsg, write, writev, sendfile, recvfrom, close.
target binary → SYS_SEND* → ptrace stop → ggvisor reads payload
→ builds Ethernet+IPv4+UDP frame → writes into UMEM → kicks AF_XDP TX ring
NIC → XDP program → bpf_redirect_map(xsks_map) → AF_XDP RX ring → ggvisor
→ strips headers → injects data into tracee via recvfrom handler
- Linux kernel ≥ 5.4 with
CONFIG_XDP_SOCKETS=y CAP_NET_ADMINor root (required for AF_XDP socket creation and ptrace)- Go ≥ 1.26 at
/home/esc/.go/bin/go(or updatescripts/test.sh) - clang + libbpf headers (for BPF program compilation, if rebuilding)
# Build ggvisor
go build -buildvcs=false -o bin/ggvisor ./cmd/ggvisor
# Build test helper binaries
go build -buildvcs=false -o bin/test-sender ./cmd/test-sender
go build -buildvcs=false -o bin/test-sendmsg ./cmd/test-sendmsg
go build -buildvcs=false -o bin/test-sendfile ./cmd/test-sendfile
go build -buildvcs=false -o bin/test-writev ./cmd/test-writev
go build -buildvcs=false -o bin/test-receiver ./cmd/test-receiverggvisor [flags] <binary> [binary-args...]
Flags:
-iface string Network interface for AF_XDP (default "eth0")
-queue int NIC queue ID (default 0)
-dst-mac string Destination MAC for TX frames — gateway or peer MAC
(default "ff:ff:ff:ff:ff:ff" = broadcast)
-xsk-map string Path to pinned BPF XSKMAP; enables RX path
(e.g. /sys/fs/bpf/xsks_map)
See examples/wrap-nc.sh for a complete walkthrough that wraps nc (netcat) under ggvisor using a veth pair, with no changes to netcat.
Short version:
# 1. Create a veth pair
ip link add ggv0 type veth peer name ggv1
ip addr add 10.99.0.1/24 dev ggv0
ip link set ggv0 up
# 2. Get peer MAC
DST_MAC=$(cat /sys/class/net/ggv1/address)
# 3. Run your binary under ggvisor
sudo ggvisor -iface ggv0 -dst-mac "$DST_MAC" \
/usr/bin/nc -u 10.99.0.2 9000All UDP traffic from nc is now sent via AF_XDP on ggv0.
To receive traffic back into the tracee, load the XDP program and pin the map:
# Compile XDP program
clang -O2 -g -target bpf \
-I/usr/include -I/usr/include/x86_64-linux-gnu \
-c bpf/xdp_prog.c -o bpf/xdp_prog.o
# Load with bpftool and pin the map
bpftool prog loadall bpf/xdp_prog.o /sys/fs/bpf/ggvisor \
pinmaps /sys/fs/bpf/ggvisor
bpftool net attach xdp id $(bpftool prog show name xdp_sock_prog -j \
| jq '.[0].id') dev ggv0
# Pass the pinned map to ggvisor
sudo ggvisor -iface ggv0 -dst-mac "$DST_MAC" \
-xsk-map /sys/fs/bpf/ggvisor/xsks_map \
/usr/bin/nc -u 10.99.0.2 9000./scripts/test.shThis runs the full Go integration suite as root. Tests spin up a veth pair + network namespace, run each send variant through ggvisor, and verify packets arrive at the receiver. TestXDPFramesOnWire uses tcpdump to confirm frames appear on the XDP interface.
Individual test:
sudo /home/esc/.go/bin/go test -v -tags integration -timeout 60s \
./tests/ -run TestSendtocmd/ggvisor/ CLI entry point
pkg/interceptor/
tracer.go ptrace loop, per-pid state machine
handlers.go per-syscall entry/exit handlers
packet.go Ethernet+IPv4+UDP frame builder/parser
socket_table.go fd → (XDP socket, addresses) mapping
mem.go process_vm_readv/writev helpers
bpf.go raw BPF syscall wrappers (map update, obj get)
pkg/xdp/
umem.go UMEM allocation (mmap + registered frames)
socket.go AF_XDP socket lifecycle, TX/RX rings
types.go ring descriptor structs
bpf/xdp_prog.c XDP kernel program (XSKMAP redirect)
tests/ Go integration test suite
examples/ Worked examples
- UDP only — TCP connection state is not tracked.
- TX path is always copy-mode XDP (works on veth and most drivers without zero-copy support).
- RX path requires a pinned XSKMAP; without it, recvfrom falls through to the kernel.
- x86-64 only (register mapping is architecture-specific).
- One XDP socket per interface/queue; multi-queue NICs need one ggvisor instance per queue.