diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51f30f2..648a41c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,40 @@ env: NODE_VERSION: '20' jobs: + # ─── Build eBPF program (nightly, bpf target, architecture-independent) ─── + build-ebpf: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install nightly Rust + BPF target + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Cache eBPF build + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust/xdp-ebpf + + - name: Build eBPF program + working-directory: rust/xdp-ebpf + run: | + cargo build \ + --target bpfel-unknown-none \ + -Z build-std=core \ + --release + + - name: Upload eBPF binary + uses: actions/upload-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf + retention-days: 1 + # ─── Test Rust ─── test-rust: + needs: build-ebpf runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -28,13 +60,22 @@ jobs: with: workspaces: rust + - name: Install libelf-dev + run: sudo apt-get update && sudo apt-get install -y libelf-dev + + - name: Download eBPF binary + uses: actions/download-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/ + - name: Rust test working-directory: rust run: cargo test # ─── Build spoof-tunnel (CGO_ENABLED=1, Rust + Go, Linux only) ─── build-tunnel: - needs: test-rust + needs: [test-rust, build-ebpf] runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -50,7 +91,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rust + - name: Install Rust (stable) uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_target }} @@ -60,9 +101,19 @@ jobs: with: workspaces: rust - - name: Install cross-compilation tools - if: matrix.goarch == 'arm64' - run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu + - name: Install cross-compilation tools & libelf-dev + run: | + sudo apt-get update + sudo apt-get install -y libelf-dev + if [ "${{ matrix.goarch }}" = "arm64" ]; then + sudo apt-get install -y gcc-aarch64-linux-gnu + fi + + - name: Download eBPF binary + uses: actions/download-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/ - name: Build Rust library working-directory: rust diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5b78e2..fb02ac2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,12 +11,48 @@ on: - 'panel/**' jobs: + # ─── Build eBPF program (nightly, bpf target) ─── + build-ebpf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install nightly Rust + BPF target + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Cache eBPF build + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust/xdp-ebpf + + - name: Install bpf-linker + run: cargo install bpf-linker + + - name: Build eBPF program + working-directory: rust/xdp-ebpf + run: | + cargo build \ + --target bpfel-unknown-none \ + -Z build-std=core \ + --release + + - name: Upload eBPF binary + uses: actions/upload-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf + retention-days: 1 + + # ─── Test Rust workspace ─── test-rust: + needs: build-ebpf runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Rust + - name: Install stable Rust uses: dtolnay/rust-toolchain@stable - name: Rust cache @@ -24,12 +60,22 @@ jobs: with: workspaces: rust + - name: Install libelf-dev (required by aya) + run: sudo apt-get update && sudo apt-get install -y libelf-dev + + - name: Download eBPF binary + uses: actions/download-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/ + - name: Test working-directory: rust run: cargo test + # ─── Test Go (depends on Rust workspace build) ─── test-go: - needs: test-rust + needs: build-ebpf runs-on: ubuntu-latest strategy: matrix: @@ -37,7 +83,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rust + - name: Install stable Rust uses: dtolnay/rust-toolchain@stable - name: Rust cache @@ -45,9 +91,18 @@ jobs: with: workspaces: rust + - name: Install libelf-dev (required by aya) + run: sudo apt-get update && sudo apt-get install -y libelf-dev + + - name: Download eBPF binary + uses: actions/download-artifact@v4 + with: + name: xdp-ebpf + path: rust/xdp-ebpf/target/bpfel-unknown-none/release/ + - name: Build Rust library working-directory: rust - run: cargo build --release + run: cargo build --release -p spoof-transport - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v5 diff --git a/.gitignore b/.gitignore index 99c662c..b871ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,7 @@ next-env.d.ts panel/backend/panel spoof-linux-amd64 spoof-panel-linux-amd64 + +# XDP eBPF build artifacts +rust/xdp-ebpf/target/ +rust/xdp-loader/target/ diff --git a/Makefile b/Makefile index 68fdf68..eac4f96 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,28 @@ -.PHONY: all core panel frontend backend clean test rust +.PHONY: all core panel frontend backend clean test rust xdp-ebpf xdp-loader # ── Build All ── all: core panel -# ── Core (spoof binary with Rust FFI) ── +# ── XDP/eBPF (kernel-side program, needs nightly) ── +xdp-ebpf: + cd rust/xdp-ebpf && cargo +nightly build \ + --target bpfel-unknown-none \ + -Z build-std=core \ + --release + +# ── XDP Loader (userspace, links into Go binary) ── +xdp-loader: xdp-ebpf + cd rust/xdp-loader && cargo build --release + +# ── Core transport (existing Rust FFI) ── rust: cd rust && cargo build --release -core: rust - CGO_ENABLED=1 go build -o spoof ./cmd/spoof/ +# ── Core binary (Go + Rust FFI + XDP) ── +core: rust xdp-loader + CGO_ENABLED=1 go build \ + -ldflags "-r $(PWD)/rust/xdp-loader/target/release" \ + -o spoof ./cmd/spoof/ # ── Panel (frontend + backend) ── panel: frontend backend @@ -29,7 +43,7 @@ dev-backend: cd panel/backend && CGO_ENABLED=0 go build -o ../../spoof-panel ./cmd/panel/ # ── Test ── -test: rust +test: rust xdp-ebpf cd rust && cargo test CGO_ENABLED=1 go test ./internal/... cd panel/backend && go vet ./... @@ -37,6 +51,8 @@ test: rust # ── Clean ── clean: cd rust && cargo clean + cd rust/xdp-ebpf && cargo clean 2>/dev/null || true + cd rust/xdp-loader && cargo clean 2>/dev/null || true rm -f spoof spoof-panel rm -rf panel/frontend/.next panel/frontend/out rm -rf panel/backend/cmd/panel/web diff --git a/cmd/spoof/main.go b/cmd/spoof/main.go index f73bdb1..44b72c0 100644 --- a/cmd/spoof/main.go +++ b/cmd/spoof/main.go @@ -54,11 +54,11 @@ func runCmd() *cobra.Command { case "local": runLocal(cfg.Listen, cfg.Remote, cfg.RemotePort, cfg.RecvPort, cfg.SpoofIP, cfg.SpoofPort, cfg.PeerSpoofIP, cfg.SpoofIPFile, - cfg.SendTransport, cfg.RecvTransport) + cfg.SendTransport, cfg.RecvTransport, cfg.XDPInterface) case "remote": runRemote(cfg.ListenPort, cfg.Forward, cfg.ClientIP, cfg.ClientPort, cfg.SpoofIP, cfg.SpoofPort, cfg.PeerSpoofIP, - cfg.SpoofIPFile, cfg.SendTransport, cfg.RecvTransport) + cfg.SpoofIPFile, cfg.SendTransport, cfg.RecvTransport, cfg.XDPInterface) default: log.Fatalf("unknown mode in config: %q", cfg.Mode) } @@ -81,6 +81,7 @@ func localCmd() *cobra.Command { spoofIPFile string sendTransport string recvTransport string + xdpInterface string configFile string ) @@ -113,7 +114,7 @@ func localCmd() *cobra.Command { log.Fatal("--spoof-ip or --spoof-ip-file is required") } - runLocal(listen, remoteAddr, remotePort, recvPort, spoofIP, spoofPort, peerSpoofIP, spoofIPFile, sendTransport, recvTransport) + runLocal(listen, remoteAddr, remotePort, recvPort, spoofIP, spoofPort, peerSpoofIP, spoofIPFile, sendTransport, recvTransport, xdpInterface) }, } @@ -127,6 +128,7 @@ func localCmd() *cobra.Command { cmd.Flags().StringVar(&spoofIPFile, "spoof-ip-file", "", "file with spoof IPs (one per line, round-robin)") cmd.Flags().StringVar(&sendTransport, "send-transport", "", "send transport: tcp, udp, icmp, icmpv6 (default: tcp)") cmd.Flags().StringVar(&recvTransport, "recv-transport", "", "recv transport: tcp, udp, icmp, icmpv6 (default: udp)") + cmd.Flags().StringVar(&xdpInterface, "xdp-interface", "", "network interface for XDP acceleration (e.g. eth0)") cmd.Flags().StringVarP(&configFile, "config", "c", "", "path to config file (CLI flags override)") return cmd @@ -144,6 +146,7 @@ func remoteCmd() *cobra.Command { spoofIPFile string sendTransport string recvTransport string + xdpInterface string configFile string ) @@ -176,7 +179,7 @@ func remoteCmd() *cobra.Command { log.Fatal("--spoof-ip or --spoof-ip-file is required") } - runRemote(listenPort, forward, clientIP, clientPort, spoofIP, spoofPort, peerSpoofIP, spoofIPFile, sendTransport, recvTransport) + runRemote(listenPort, forward, clientIP, clientPort, spoofIP, spoofPort, peerSpoofIP, spoofIPFile, sendTransport, recvTransport, xdpInterface) }, } @@ -190,6 +193,7 @@ func remoteCmd() *cobra.Command { cmd.Flags().StringVar(&spoofIPFile, "spoof-ip-file", "", "file with spoof IPs (one per line, round-robin)") cmd.Flags().StringVar(&sendTransport, "send-transport", "", "send transport: tcp, udp, icmp, icmpv6 (default: udp)") cmd.Flags().StringVar(&recvTransport, "recv-transport", "", "recv transport: tcp, udp, icmp, icmpv6 (default: tcp)") + cmd.Flags().StringVar(&xdpInterface, "xdp-interface", "", "network interface for XDP acceleration (e.g. eth0)") cmd.Flags().StringVarP(&configFile, "config", "c", "", "path to config file (CLI flags override)") return cmd @@ -211,7 +215,7 @@ func loadSpoofIPs(spoofIP, spoofIPFile string) ([]net.IP, error) { return []net.IP{ip}, nil } -func runLocal(listen, remoteAddr string, remotePort, recvPort int, spoofIP string, spoofPort int, peerSpoofIP, spoofIPFile, sendTransport, recvTransport string) { +func runLocal(listen, remoteAddr string, remotePort, recvPort int, spoofIP string, spoofPort int, peerSpoofIP, spoofIPFile, sendTransport, recvTransport, xdpInterface string) { rIP := net.ParseIP(remoteAddr) if rIP == nil { log.Fatalf("invalid remote IP: %s", remoteAddr) @@ -252,6 +256,7 @@ func runLocal(listen, remoteAddr string, remotePort, recvPort int, spoofIP strin PeerSpoofIP: psIP, SendTransport: sendTransport, RecvTransport: recvTransport, + XDPInterface: xdpInterface, } l, err := relay.NewLocal(cfg) @@ -284,7 +289,7 @@ func runLocal(listen, remoteAddr string, remotePort, recvPort int, spoofIP strin l.Run() } -func runRemote(listenPort int, forward, clientIP string, clientPort int, spoofIP string, spoofPort int, peerSpoofIP string, spoofIPFile, sendTransport, recvTransport string) { +func runRemote(listenPort int, forward, clientIP string, clientPort int, spoofIP string, spoofPort int, peerSpoofIP string, spoofIPFile, sendTransport, recvTransport, xdpInterface string) { cIP := net.ParseIP(clientIP) if cIP == nil { log.Fatalf("invalid client-ip: %s", clientIP) @@ -322,6 +327,7 @@ func runRemote(listenPort int, forward, clientIP string, clientPort int, spoofIP PeerSpoofIP: psIP, SendTransport: sendTransport, RecvTransport: recvTransport, + XDPInterface: xdpInterface, } r, err := relay.NewRemote(cfg) diff --git a/internal/config/config.go b/internal/config/config.go index c38dfbc..7568936 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,6 +34,9 @@ type Config struct { // Transport selection per direction SendTransport string `json:"send_transport,omitempty"` // "tcp", "udp", "icmp", "icmpv6" RecvTransport string `json:"recv_transport,omitempty"` // "tcp", "udp", "icmp", "icmpv6" + + // XDP/eBPF acceleration (receive path) + XDPInterface string `json:"xdp_interface,omitempty"` // NIC to attach XDP to (e.g. "eth0") } func Load(path string) (*Config, error) { diff --git a/internal/relay/local.go b/internal/relay/local.go index cb6f240..9d6dd1e 100644 --- a/internal/relay/local.go +++ b/internal/relay/local.go @@ -21,6 +21,7 @@ type LocalConfig struct { PeerSpoofIP net.IP SendTransport string // "tcp", "udp", "icmp", "icmpv6" RecvTransport string // "tcp", "udp", "icmp", "icmpv6" + XDPInterface string // network interface for XDP (empty = disabled) } type Local struct { @@ -89,9 +90,11 @@ func NewLocal(cfg LocalConfig) (*Local, error) { } recver, err := transport.NewReceiver(cfg.RecvTransport, transport.ReceiverConfig{ - ListenPort: cfg.RecvPort, - PeerSpoofIP: cfg.PeerSpoofIP, - BufferSize: 4 * 1024 * 1024, + ListenPort: cfg.RecvPort, + PeerSpoofIP: cfg.PeerSpoofIP, + BufferSize: 4 * 1024 * 1024, + UseXDP: cfg.XDPInterface != "", + XDPInterface: cfg.XDPInterface, }) if err != nil { udpConn.Close() diff --git a/internal/relay/remote.go b/internal/relay/remote.go index c0e7c60..92924e9 100644 --- a/internal/relay/remote.go +++ b/internal/relay/remote.go @@ -21,6 +21,7 @@ type RemoteConfig struct { PeerSpoofIP net.IP SendTransport string // "tcp", "udp", "icmp", "icmpv6" RecvTransport string // "tcp", "udp", "icmp", "icmpv6" + XDPInterface string // network interface for XDP (empty = disabled) } type Remote struct { @@ -54,9 +55,11 @@ func NewRemote(cfg RemoteConfig) (*Remote, error) { } recver, err := transport.NewReceiver(cfg.RecvTransport, transport.ReceiverConfig{ - ListenPort: cfg.ListenPort, - PeerSpoofIP: cfg.PeerSpoofIP, - BufferSize: 4 * 1024 * 1024, + ListenPort: cfg.ListenPort, + PeerSpoofIP: cfg.PeerSpoofIP, + BufferSize: 4 * 1024 * 1024, + UseXDP: cfg.XDPInterface != "", + XDPInterface: cfg.XDPInterface, }) if err != nil { if icmpSuppressed { diff --git a/internal/transport/factory.go b/internal/transport/factory.go index 9d7ac85..6597873 100644 --- a/internal/transport/factory.go +++ b/internal/transport/factory.go @@ -1,6 +1,9 @@ package transport -import "fmt" +import ( + "fmt" + "log" +) // Supported transport types. const ( @@ -27,7 +30,24 @@ func NewSender(transport string, cfg SenderConfig) (Sender, error) { } // NewReceiver creates a Receiver for the given transport type. +// If XDP is enabled in config, it tries XDP first and falls back to raw socket. func NewReceiver(transport string, cfg ReceiverConfig) (Receiver, error) { + // Try XDP first if enabled + if cfg.UseXDP && cfg.XDPInterface != "" { + protoNum := transportToProto(transport) + log.Printf("[transport] XDP requested on %q (proto=%s→%d)", cfg.XDPInterface, transport, protoNum) + if protoNum > 0 { + xdp, err := NewXDPReceiver(cfg.XDPInterface, cfg.PeerSpoofIP, cfg.ListenPort, protoNum) + if err == nil { + log.Printf("[transport] ✓ XDP attached to %q (proto=%d, port=%d)", cfg.XDPInterface, protoNum, cfg.ListenPort) + return xdp, nil + } + // XDP failed — log and fall back + log.Printf("[transport] XDP attach to %q failed: %v", cfg.XDPInterface, err) + log.Printf("[transport] falling back to raw socket receiver") + } + } + switch transport { case TransportTCP: return NewTCPReceiver(cfg) @@ -41,3 +61,17 @@ func NewReceiver(transport string, cfg ReceiverConfig) (Receiver, error) { return nil, fmt.Errorf("unknown recv transport: %q (use tcp, udp, icmp, icmpv6)", transport) } } + +// transportToProto maps transport names to IP protocol numbers for XDP. +func transportToProto(transport string) uint8 { + switch transport { + case TransportTCP: + return 6 + case TransportUDP: + return 17 + case TransportICMP: + return 1 + default: + return 0 // unsupported for XDP + } +} diff --git a/internal/transport/ffi.go b/internal/transport/ffi.go index 182b6c4..9f607e1 100644 --- a/internal/transport/ffi.go +++ b/internal/transport/ffi.go @@ -1,7 +1,7 @@ package transport /* -#cgo LDFLAGS: -L${SRCDIR}/../../rust/target/release -lspoof_transport -lm -ldl -lpthread +#cgo LDFLAGS: -L${SRCDIR}/../../rust/target/release -lspoof_transport -lelf -lz -lm -ldl -lpthread #include "spoof_transport.h" */ import "C" diff --git a/internal/transport/spoof_transport.h b/internal/transport/spoof_transport.h index a0c3856..544a5fa 100644 --- a/internal/transport/spoof_transport.h +++ b/internal/transport/spoof_transport.h @@ -42,4 +42,10 @@ SpoofHandle spoof_icmpv6_receiver_new(uint8_t* peer_ip, int32_t buf_size); int32_t spoof_icmpv6_receiver_recv(SpoofHandle h, uint8_t* out_buf, uintptr_t out_buf_len, uint8_t* src_ip_out); void spoof_icmpv6_receiver_close(SpoofHandle h); +// XDP Receiver (high-performance kernel-bypass receive) +SpoofHandle spoof_xdp_receiver_new(const char* interface_name, uint8_t* peer_spoof_ip, uint16_t listen_port, uint8_t protocol); +int32_t spoof_xdp_receiver_recv(SpoofHandle h, uint8_t* out_buf, uintptr_t out_buf_len, uint8_t* src_ip_out, uint16_t* src_port_out); +void spoof_xdp_receiver_close(SpoofHandle h); +int32_t spoof_xdp_available(void); + #endif diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 1d6908d..6b9d946 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -24,9 +24,11 @@ type SenderConfig struct { // ReceiverConfig holds config shared by all receivers. type ReceiverConfig struct { - ListenPort uint16 // Port to filter incoming packets on - PeerSpoofIP net.IP // Expected source IP for filtering (nil = accept all) - BufferSize int // SO_RCVBUF size + ListenPort uint16 // Port to filter incoming packets on + PeerSpoofIP net.IP // Expected source IP for filtering (nil = accept all) + BufferSize int // SO_RCVBUF size + UseXDP bool // Enable XDP/eBPF acceleration + XDPInterface string // Network interface for XDP (e.g. "eth0") } // Validate checks the sender config. diff --git a/internal/transport/xdp_receiver.go b/internal/transport/xdp_receiver.go new file mode 100644 index 0000000..6245466 --- /dev/null +++ b/internal/transport/xdp_receiver.go @@ -0,0 +1,138 @@ +package transport + +// #include "spoof_transport.h" +// #include +import "C" +import ( + "fmt" + "net" + "sync/atomic" + "unsafe" +) + +// XDPReceiver provides high-performance packet reception using eBPF/XDP. +// Packets are filtered and parsed in kernel space before being delivered +// via a BPF ring buffer, eliminating sk_buff allocation overhead. +// +// Falls back to raw socket receivers if XDP is not available. +type XDPReceiver struct { + handle C.SpoofHandle + closed atomic.Bool + + pktCh chan receivedPacket + doneCh chan struct{} +} + +// XDPAvailable checks if XDP/eBPF is supported on this kernel. +func XDPAvailable() bool { + return C.spoof_xdp_available() == 1 +} + +// NewXDPReceiver creates an XDP-based receiver attached to a network interface. +// - iface: network interface name (e.g. "eth0", "enp1s0") +// - peerSpoofIP: only accept packets from this source IP (nil = accept all) +// - listenPort: only accept packets to this dest port (0 = accept all) +// - protocol: IP protocol number (6=TCP, 17=UDP, 1=ICMP) +func NewXDPReceiver(iface string, peerSpoofIP net.IP, listenPort uint16, protocol uint8) (*XDPReceiver, error) { + ifaceC := C.CString(iface) + defer C.free(unsafe.Pointer(ifaceC)) + + var peerIP *C.uint8_t + if peerSpoofIP != nil { + ip4 := peerSpoofIP.To4() + if ip4 != nil { + peerIP = (*C.uint8_t)(unsafe.Pointer(&ip4[0])) + } + } + + h := C.spoof_xdp_receiver_new( + ifaceC, + peerIP, + C.uint16_t(listenPort), + C.uint8_t(protocol), + ) + if h == nil { + return nil, fmt.Errorf("XDP attach to %q failed (need root + kernel ≥5.8)", iface) + } + + r := &XDPReceiver{ + handle: h, + pktCh: make(chan receivedPacket, 4096), + doneCh: make(chan struct{}), + } + go r.readLoop() + return r, nil +} + +// readLoop calls the Rust XDP recv in a loop, delivering packets to pktCh. +func (r *XDPReceiver) readLoop() { + buf := make([]byte, 65536) + var srcIPBuf [4]byte + var srcPort C.uint16_t + + for { + select { + case <-r.doneCh: + return + default: + } + + n := C.spoof_xdp_receiver_recv( + r.handle, + (*C.uint8_t)(unsafe.Pointer(&buf[0])), + C.uintptr_t(len(buf)), + (*C.uint8_t)(unsafe.Pointer(&srcIPBuf[0])), + &srcPort, + ) + + if n < 0 { + if r.closed.Load() { + return + } + continue + } + if n == 0 { + continue + } + + data := make([]byte, int(n)) + copy(data, buf[:int(n)]) + + pkt := receivedPacket{ + data: data, + srcIP: net.IPv4(srcIPBuf[0], srcIPBuf[1], srcIPBuf[2], srcIPBuf[3]), + srcPort: uint16(srcPort), + } + + select { + case r.pktCh <- pkt: + default: + // Drop oldest if channel full + select { + case <-r.pktCh: + default: + } + r.pktCh <- pkt + } + } +} + +// Receive blocks until a packet arrives from XDP. +func (r *XDPReceiver) Receive() ([]byte, net.IP, uint16, error) { + pkt, ok := <-r.pktCh + if !ok { + return nil, nil, 0, ErrConnectionClosed + } + return pkt.data, pkt.srcIP, pkt.srcPort, nil +} + +// Close detaches the XDP program and cleans up. +func (r *XDPReceiver) Close() error { + if r.closed.Swap(true) { + return nil + } + close(r.doneCh) + C.spoof_xdp_receiver_close(r.handle) + close(r.pktCh) + return nil +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d9526e1..ab981b1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,15 +2,542 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "aya" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +dependencies = [ + "assert_matches", + "aya-obj", + "bitflags", + "bytes", + "libc", + "log", + "object", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "aya-log" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b600d806c1d07d3b81ab5f4a2a95fd80f479a0d3f1d68f29064d660865f85f02" +dependencies = [ + "aya", + "aya-log-common", + "bytes", + "log", + "thiserror", + "tokio", +] + +[[package]] +name = "aya-log-common" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befef9fe882e63164a2ba0161874e954648a72b0e1c4b361f532d590638c4eec" +dependencies = [ + "num_enum", +] + +[[package]] +name = "aya-obj" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +dependencies = [ + "bytes", + "core-error", + "hashbrown 0.15.5", + "log", + "object", + "thiserror", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-error" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" +dependencies = [ + "version_check", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "libc" version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spoof-transport" version = "0.1.0" dependencies = [ "libc", + "xdp-loader", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xdp-loader" +version = "0.1.0" +dependencies = [ + "aya", + "aya-log", + "env_logger", + "libc", + "log", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9736334..857680d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,13 +1,6 @@ -[package] -name = "spoof-transport" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["staticlib"] - -[dependencies] -libc = "0.2" +[workspace] +members = ["spoof-transport", "xdp-loader"] +exclude = ["xdp-ebpf"] [profile.release] opt-level = 3 diff --git a/rust/spoof-transport/Cargo.toml b/rust/spoof-transport/Cargo.toml new file mode 100644 index 0000000..55dba1e --- /dev/null +++ b/rust/spoof-transport/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "spoof-transport" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +libc = "0.2" +xdp-loader = { path = "../xdp-loader" } diff --git a/rust/src/checksum.rs b/rust/spoof-transport/src/checksum.rs similarity index 100% rename from rust/src/checksum.rs rename to rust/spoof-transport/src/checksum.rs diff --git a/rust/src/icmp_receiver.rs b/rust/spoof-transport/src/icmp_receiver.rs similarity index 100% rename from rust/src/icmp_receiver.rs rename to rust/spoof-transport/src/icmp_receiver.rs diff --git a/rust/src/icmp_sender.rs b/rust/spoof-transport/src/icmp_sender.rs similarity index 100% rename from rust/src/icmp_sender.rs rename to rust/spoof-transport/src/icmp_sender.rs diff --git a/rust/src/icmpv6_receiver.rs b/rust/spoof-transport/src/icmpv6_receiver.rs similarity index 100% rename from rust/src/icmpv6_receiver.rs rename to rust/spoof-transport/src/icmpv6_receiver.rs diff --git a/rust/src/icmpv6_sender.rs b/rust/spoof-transport/src/icmpv6_sender.rs similarity index 100% rename from rust/src/icmpv6_sender.rs rename to rust/spoof-transport/src/icmpv6_sender.rs diff --git a/rust/src/lib.rs b/rust/spoof-transport/src/lib.rs similarity index 99% rename from rust/src/lib.rs rename to rust/spoof-transport/src/lib.rs index 5925f6e..fe2fbc4 100644 --- a/rust/src/lib.rs +++ b/rust/spoof-transport/src/lib.rs @@ -1,6 +1,9 @@ //! spoof-transport: High-performance raw socket transport layer. //! //! Exposes a C ABI for Go via CGo. Supports TCP, UDP, ICMP, ICMPv6. +//! Also links xdp-loader for XDP/eBPF kernel-bypass receive support. + +extern crate xdp_loader; mod checksum; mod raw_socket; diff --git a/rust/src/raw_socket.rs b/rust/spoof-transport/src/raw_socket.rs similarity index 100% rename from rust/src/raw_socket.rs rename to rust/spoof-transport/src/raw_socket.rs diff --git a/rust/src/tcp_receiver.rs b/rust/spoof-transport/src/tcp_receiver.rs similarity index 100% rename from rust/src/tcp_receiver.rs rename to rust/spoof-transport/src/tcp_receiver.rs diff --git a/rust/src/tcp_sender.rs b/rust/spoof-transport/src/tcp_sender.rs similarity index 100% rename from rust/src/tcp_sender.rs rename to rust/spoof-transport/src/tcp_sender.rs diff --git a/rust/src/udp_sender.rs b/rust/spoof-transport/src/udp_sender.rs similarity index 100% rename from rust/src/udp_sender.rs rename to rust/spoof-transport/src/udp_sender.rs diff --git a/rust/xdp-ebpf/.gitignore b/rust/xdp-ebpf/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/rust/xdp-ebpf/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/rust/xdp-ebpf/Cargo.lock b/rust/xdp-ebpf/Cargo.lock new file mode 100644 index 0000000..c34d7c2 --- /dev/null +++ b/rust/xdp-ebpf/Cargo.lock @@ -0,0 +1,253 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "aya-build" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bc42f3c5ddacc34eca28a420b47e3cbb3f0f484137cb2bf1ad2153d0eae52a" +dependencies = [ + "anyhow", + "cargo_metadata", +] + +[[package]] +name = "aya-ebpf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8dbaf5409a1a0982e5c9bdc0f499a55fe5ead39fe9c846012053faf0d404f73" +dependencies = [ + "aya-ebpf-bindings", + "aya-ebpf-cty", + "aya-ebpf-macros", + "rustversion", +] + +[[package]] +name = "aya-ebpf-bindings" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ee8e6a617f040d8da7565ec4010aea75e33cda4662f64c019c66ee97d17889" +dependencies = [ + "aya-build", + "aya-ebpf-cty", +] + +[[package]] +name = "aya-ebpf-cty" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f33396742e7fd0f519c1e0de5141d84e1a8df69146a557c08cc222b0ceace4" +dependencies = [ + "aya-build", +] + +[[package]] +name = "aya-ebpf-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fd02363736177e7e91d6c95d7effbca07be87502c7b5b32fc194aed8b177a0" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "xdp-ebpf" +version = "0.1.0" +dependencies = [ + "aya-ebpf", + "aya-ebpf-bindings", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/xdp-ebpf/Cargo.toml b/rust/xdp-ebpf/Cargo.toml new file mode 100644 index 0000000..e8f7a77 --- /dev/null +++ b/rust/xdp-ebpf/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "xdp-ebpf" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "xdp-ebpf" +path = "src/main.rs" + +[dependencies] +aya-ebpf = "0.1" +aya-ebpf-bindings = "0.1" + +[profile.dev] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 +debug = 2 +strip = false diff --git a/rust/xdp-ebpf/src/main.rs b/rust/xdp-ebpf/src/main.rs new file mode 100644 index 0000000..ba6718e --- /dev/null +++ b/rust/xdp-ebpf/src/main.rs @@ -0,0 +1,251 @@ +#![no_std] +#![no_main] +#![feature(asm_experimental_arch)] + +//! XDP program for high-performance spoofed packet receive & payload extraction. +//! +//! Runs at NIC driver level (XDP hook) before sk_buff allocation. +//! Uses bpf_xdp_load_bytes() for payload copy to avoid BPF verifier issues +//! with variable-offset packet pointer access. + +use aya_ebpf::{ + bindings::xdp_action, + macros::{map, xdp}, + maps::{HashMap, RingBuf}, + programs::XdpContext, +}; + +use core::mem; + +// ── Fixed offsets (constants for BPF verifier) ── +const ETH_HDR_LEN: usize = 14; +const IP_HDR_LEN: usize = 20; +const TCP_HDR_LEN: usize = 20; +const UDP_HDR_LEN: usize = 8; +const ICMP_HDR_LEN: usize = 8; + +const PROTO_ICMP: u8 = 1; +const PROTO_TCP: u8 = 6; +const PROTO_UDP: u8 = 17; + +const ETHERTYPE_IPV4_BE: u16 = 0x0800u16.to_be(); + +/// Configuration entry stored in BPF HashMap. +#[repr(C)] +pub struct XdpConfig { + pub peer_ip: u32, + pub dst_port: u16, + pub protocol: u8, + pub _pad: u8, +} + +/// Packet metadata header prepended to payload in the ring buffer. +#[repr(C)] +pub struct PktMeta { + pub src_ip: u32, + pub src_port: u16, + pub payload_len: u16, +} + +#[map] +static CONFIG: HashMap = HashMap::with_max_entries(1, 0); + +#[map] +static PAYLOADS: RingBuf = RingBuf::with_byte_size(16 * 1024 * 1024, 0); + +#[map] +static PKT_COUNT: HashMap = HashMap::with_max_entries(1, 0); + +#[xdp] +pub fn spoof_xdp_recv(ctx: XdpContext) -> u32 { + match try_process(&ctx) { + Ok(action) => action, + Err(_) => xdp_action::XDP_PASS, + } +} + +#[inline(always)] +fn try_process(ctx: &XdpContext) -> Result { + let data = ctx.data(); + let data_end = ctx.data_end(); + + // ── 1. Ethernet header (14 bytes) ── + if data + ETH_HDR_LEN > data_end { + return Ok(xdp_action::XDP_PASS); + } + let ether_type = unsafe { *(data as *const u16).add(6) }; + if ether_type != ETHERTYPE_IPV4_BE { + return Ok(xdp_action::XDP_PASS); + } + + // ── 2. IPv4 header (fixed 20 bytes) ── + if data + ETH_HDR_LEN + IP_HDR_LEN > data_end { + return Ok(xdp_action::XDP_PASS); + } + let ip_base = data + ETH_HDR_LEN; + let src_ip = unsafe { *((ip_base + 12) as *const u32) }; + let ip_proto = unsafe { *((ip_base + 9) as *const u8) }; + + // ── 2b. Skip fragmented packets ── + // IP header bytes 6-7: 3 flag bits + 13-bit fragment offset. + // MF (More Fragments) = bit 13 (0x2000), frag offset = bits 0-12 (0x1FFF). + // If either is set, this is a fragment — let the kernel handle reassembly. + let flags_frag = unsafe { *((ip_base + 6) as *const u16) }; + if u16::from_be(flags_frag) & 0x3FFF != 0 { + return Ok(xdp_action::XDP_PASS); + } + + // ── 3. Load config ── + let key: u32 = 0; + let cfg = match unsafe { CONFIG.get(&key) } { + Some(c) => c, + None => return Ok(xdp_action::XDP_PASS), + }; + + // ── 4. Filter by protocol ── + if cfg.protocol != 0 && ip_proto != cfg.protocol { + return Ok(xdp_action::XDP_PASS); + } + + // ── 5. Filter by peer spoof IP ── + if cfg.peer_ip != 0 && src_ip != cfg.peer_ip { + return Ok(xdp_action::XDP_PASS); + } + + // ── 6. Parse transport header using FIXED offsets ── + let transport_base = data + ETH_HDR_LEN + IP_HDR_LEN; + + let (src_port, payload_offset): (u16, u32) = match ip_proto { + PROTO_TCP => { + if data + ETH_HDR_LEN + IP_HDR_LEN + TCP_HDR_LEN > data_end { + return Ok(xdp_action::XDP_PASS); + } + let dst_port = u16::from_be(unsafe { *((transport_base + 2) as *const u16) }); + if cfg.dst_port != 0 && dst_port != cfg.dst_port { + return Ok(xdp_action::XDP_PASS); + } + let sp = u16::from_be(unsafe { *((transport_base) as *const u16) }); + (sp, (ETH_HDR_LEN + IP_HDR_LEN + TCP_HDR_LEN) as u32) + } + PROTO_UDP => { + if data + ETH_HDR_LEN + IP_HDR_LEN + UDP_HDR_LEN > data_end { + return Ok(xdp_action::XDP_PASS); + } + let dst_port = u16::from_be(unsafe { *((transport_base + 2) as *const u16) }); + if cfg.dst_port != 0 && dst_port != cfg.dst_port { + return Ok(xdp_action::XDP_PASS); + } + let sp = u16::from_be(unsafe { *((transport_base) as *const u16) }); + (sp, (ETH_HDR_LEN + IP_HDR_LEN + UDP_HDR_LEN) as u32) + } + PROTO_ICMP => { + if data + ETH_HDR_LEN + IP_HDR_LEN + ICMP_HDR_LEN > data_end { + return Ok(xdp_action::XDP_PASS); + } + (0u16, (ETH_HDR_LEN + IP_HDR_LEN + ICMP_HDR_LEN) as u32) + } + _ => return Ok(xdp_action::XDP_PASS), + }; + + // ── 7. Calculate payload length ── + if data + (payload_offset as usize) >= data_end { + return Ok(xdp_action::XDP_PASS); + } + let payload_len = (data_end - (data + payload_offset as usize)) as u32; + if payload_len == 0 || payload_len > 1500 { + return Ok(xdp_action::XDP_PASS); + } + + // ── 8. Reserve ring buffer entry ── + let meta_size = mem::size_of::() as u32; // 8 + let total_len = meta_size + payload_len; + if total_len > 2064 { + return Ok(xdp_action::XDP_PASS); + } + + let mut entry = match PAYLOADS.reserve::<[u8; 2064]>(0) { + Some(e) => e, + None => return Ok(xdp_action::XDP_PASS), + }; + + // ── 9. Write metadata ── + let meta = PktMeta { + src_ip, + src_port, + payload_len: payload_len as u16, + }; + let entry_ptr = entry.as_mut_ptr() as *mut u8; + let meta_ptr = &meta as *const PktMeta as *const u8; + unsafe { + *entry_ptr.add(0) = *meta_ptr.add(0); + *entry_ptr.add(1) = *meta_ptr.add(1); + *entry_ptr.add(2) = *meta_ptr.add(2); + *entry_ptr.add(3) = *meta_ptr.add(3); + *entry_ptr.add(4) = *meta_ptr.add(4); + *entry_ptr.add(5) = *meta_ptr.add(5); + *entry_ptr.add(6) = *meta_ptr.add(6); + *entry_ptr.add(7) = *meta_ptr.add(7); + } + + // ── 10. Copy payload using bpf_xdp_load_bytes ── + // The BPF verifier needs to know the length argument is in [1, MAX]. + // LLVM cannot propagate range info across opaque asm boundaries, so we use + // a single asm block that: masks the value AND conditionally jumps past the + // helper call if it's zero. The verifier sees the branch and knows that + // on the fall-through path the value is >= 1. + let payload_dst = unsafe { entry_ptr.add(meta_size as usize) }; + let ctx_ptr = ctx.ctx; + let copied_len: i64; + + // Safety: payload_offset and payload_len are both bounded and checked above. + unsafe { + core::arch::asm!( + // r2 = payload_len & 0x7FF → verifier: r2 ∈ [0, 2047] + "r2 = r4", + "r2 &= 2047", + // if r2 == 0, skip the helper call + "if r2 == 0 goto +6", + // bpf_xdp_load_bytes(ctx, offset, dst, len) — args in r1-r4 + "r1 = r7", // r1 = ctx + "r4 = r2", // r4 = len (verifier: [1, 2047] on this path) + "r2 = r8", // r2 = payload_offset + "r3 = r5", // r3 = dst buffer + "call 189", // bpf_xdp_load_bytes + "goto +1", + // zero case: set return to -1 (error) + "r0 = -1", + in("r4") payload_len, + in("r5") payload_dst, + in("r7") ctx_ptr, + in("r8") payload_offset, + out("r0") copied_len, + // clobbers + lateout("r1") _, + lateout("r2") _, + lateout("r3") _, + lateout("r4") _, + ); + } + if copied_len < 0 { + entry.discard(0); + return Ok(xdp_action::XDP_PASS); + } + + // ── 11. Submit ── + entry.submit(0); + + // ── 12. Update counter ── + let ck: u32 = 0; + if let Some(c) = unsafe { PKT_COUNT.get(&ck) } { + let _ = PKT_COUNT.insert(&ck, &(*c + 1), 0); + } else { + let _ = PKT_COUNT.insert(&ck, &1u64, 0); + } + + Ok(xdp_action::XDP_DROP) +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/rust/xdp-loader/Cargo.lock b/rust/xdp-loader/Cargo.lock new file mode 100644 index 0000000..8c90c1b --- /dev/null +++ b/rust/xdp-loader/Cargo.lock @@ -0,0 +1,535 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "aya" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +dependencies = [ + "assert_matches", + "aya-obj", + "bitflags", + "bytes", + "libc", + "log", + "object", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "aya-log" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b600d806c1d07d3b81ab5f4a2a95fd80f479a0d3f1d68f29064d660865f85f02" +dependencies = [ + "aya", + "aya-log-common", + "bytes", + "log", + "thiserror", + "tokio", +] + +[[package]] +name = "aya-log-common" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befef9fe882e63164a2ba0161874e954648a72b0e1c4b361f532d590638c4eec" +dependencies = [ + "num_enum", +] + +[[package]] +name = "aya-obj" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +dependencies = [ + "bytes", + "core-error", + "hashbrown 0.15.5", + "log", + "object", + "thiserror", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-error" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" +dependencies = [ + "version_check", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xdp-loader" +version = "0.1.0" +dependencies = [ + "aya", + "aya-log", + "env_logger", + "libc", + "log", +] diff --git a/rust/xdp-loader/Cargo.toml b/rust/xdp-loader/Cargo.toml new file mode 100644 index 0000000..4c7e687 --- /dev/null +++ b/rust/xdp-loader/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "xdp-loader" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +aya = "0.13" +aya-log = "0.2" +libc = "0.2" +log = "0.4" +env_logger = "0.11" + +[build-dependencies] + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = "symbols" diff --git a/rust/xdp-loader/build.rs b/rust/xdp-loader/build.rs new file mode 100644 index 0000000..f5819d6 --- /dev/null +++ b/rust/xdp-loader/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Tell cargo to re-run this build script (and recompile) + // whenever the eBPF binary changes. + println!("cargo:rerun-if-changed=../xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf"); +} diff --git a/rust/xdp-loader/examples/test_load.rs b/rust/xdp-loader/examples/test_load.rs new file mode 100644 index 0000000..f72d2d2 --- /dev/null +++ b/rust/xdp-loader/examples/test_load.rs @@ -0,0 +1,27 @@ +fn main() { + #[repr(C, align(8))] + struct AlignedElf([u8; N]); + + const ALIGNED: &AlignedElf<{ include_bytes!( + "../../xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf" + ).len() }> = &AlignedElf(*include_bytes!( + "../../xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf" + )); + + let elf_bytes: &[u8] = &ALIGNED.0; + println!("ELF size: {} bytes", elf_bytes.len()); + println!("Magic: {:02x} {:02x} {:02x} {:02x}", elf_bytes[0], elf_bytes[1], elf_bytes[2], elf_bytes[3]); + println!("Alignment: {}", elf_bytes.as_ptr() as usize % 8); + + match aya::EbpfLoader::new().load(elf_bytes) { + Ok(bpf) => { + println!("Load OK!"); + for (name, _) in bpf.programs() { + println!(" program: {}", name); + } + } + Err(e) => { + println!("Load error: {:?}", e); + } + } +} diff --git a/rust/xdp-loader/src/lib.rs b/rust/xdp-loader/src/lib.rs new file mode 100644 index 0000000..1a84129 --- /dev/null +++ b/rust/xdp-loader/src/lib.rs @@ -0,0 +1,280 @@ +//! XDP Loader — userspace component that loads the eBPF XDP program, +//! configures BPF maps, and reads packets from the ring buffer. +//! +//! Exports a C ABI matching the existing spoof_*_receiver_* pattern +//! so Go can use it via CGo with zero changes to the relay logic. + +use aya::maps::{HashMap, RingBuf}; +use aya::programs::{Xdp, XdpFlags}; +use aya::{EbpfLoader, Pod}; +use std::ffi::CStr; +use std::os::fd::AsRawFd; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Matches the eBPF-side struct. +#[repr(C)] +#[derive(Clone, Copy)] +struct XdpConfig { + peer_ip: u32, + dst_port: u16, + protocol: u8, + _pad: u8, +} + +unsafe impl Pod for XdpConfig {} + +/// Matches the eBPF-side struct. +#[repr(C)] +#[derive(Clone, Copy)] +struct PktMeta { + src_ip: u32, + src_port: u16, + payload_len: u16, +} + +/// Internal state for one XDP receiver instance. +struct XdpReceiver { + ring: RingBuf, + closed: AtomicBool, + interface: String, + // Keep bpf alive so the XDP program stays attached. + // It's not directly used after setup, but dropping it detaches the program. + _bpf: aya::Ebpf, +} + +// We manage thread safety ourselves +unsafe impl Send for XdpReceiver {} +unsafe impl Sync for XdpReceiver {} + +/// Embedded eBPF bytecode — compiled from xdp-ebpf crate. +/// Included at compile time so the binary is self-contained. +static XDP_ELF_RAW: &[u8] = include_bytes!( + "../../xdp-ebpf/target/bpfel-unknown-none/release/xdp-ebpf" +); + +/// Return an aligned copy of the eBPF ELF. +/// The `object` crate requires 8-byte alignment for ELF header parsing. +fn aligned_elf_copy() -> Vec { + // Vec is always properly aligned (allocator guarantees ≥ pointer alignment) + XDP_ELF_RAW.to_vec() +} + +/// Create and attach an XDP receiver. +/// +/// # Safety +/// `interface_name` must be a valid C string. +/// `peer_spoof_ip` may be null (accept all) or point to 4 bytes. +#[no_mangle] +pub unsafe extern "C" fn spoof_xdp_receiver_new( + interface_name: *const libc::c_char, + peer_spoof_ip: *const u8, + listen_port: u16, + protocol: u8, +) -> *mut libc::c_void { + let iface = match CStr::from_ptr(interface_name).to_str() { + Ok(s) => s.to_string(), + Err(_) => return std::ptr::null_mut(), + }; + + let _ = env_logger::try_init(); + + // Load eBPF program from aligned copy of embedded ELF + let elf_data = aligned_elf_copy(); + let mut bpf = match EbpfLoader::new().load(&elf_data) { + Ok(b) => b, + Err(e) => { + log::error!("[xdp-loader] failed to load eBPF: {}", e); + return std::ptr::null_mut(); + } + }; + + // Attach XDP program to interface + { + let prog: &mut Xdp = match bpf.program_mut("spoof_xdp_recv") { + Some(p) => match p.try_into() { + Ok(x) => x, + Err(e) => { + log::error!("[xdp-loader] program type error: {}", e); + return std::ptr::null_mut(); + } + }, + None => { + log::error!("[xdp-loader] program 'spoof_xdp_recv' not found"); + return std::ptr::null_mut(); + } + }; + + if let Err(e) = prog.load() { + log::error!("[xdp-loader] failed to load program: {}", e); + return std::ptr::null_mut(); + } + + // Try SKB mode first (works everywhere), then native driver mode + let attach_result = prog + .attach(&iface, XdpFlags::SKB_MODE) + .or_else(|_| prog.attach(&iface, XdpFlags::default())); + + if let Err(e) = attach_result { + log::error!("[xdp-loader] attach to '{}' failed: {}", iface, e); + return std::ptr::null_mut(); + } + + log::info!("[xdp-loader] attached XDP to '{}'", iface); + } + + // Write config to BPF map + { + let peer_ip = if peer_spoof_ip.is_null() { + 0u32 + } else { + u32::from_ne_bytes([ + *peer_spoof_ip, + *peer_spoof_ip.add(1), + *peer_spoof_ip.add(2), + *peer_spoof_ip.add(3), + ]) + }; + + let cfg = XdpConfig { + peer_ip, + dst_port: listen_port, + protocol, + _pad: 0, + }; + + let mut config_map: HashMap<_, u32, XdpConfig> = + match HashMap::try_from(bpf.map_mut("CONFIG").unwrap()) { + Ok(m) => m, + Err(e) => { + log::error!("[xdp-loader] CONFIG map error: {}", e); + return std::ptr::null_mut(); + } + }; + + if let Err(e) = config_map.insert(0u32, cfg, 0) { + log::error!("[xdp-loader] config insert error: {}", e); + return std::ptr::null_mut(); + } + } + + // Get ring buffer handle (take ownership of the map) + let ring: RingBuf = + match RingBuf::try_from(bpf.take_map("PAYLOADS").unwrap()) { + Ok(r) => r, + Err(e) => { + log::error!("[xdp-loader] PAYLOADS ring error: {}", e); + return std::ptr::null_mut(); + } + }; + + let receiver = Box::new(XdpReceiver { + ring, + closed: AtomicBool::new(false), + interface: iface.clone(), + _bpf: bpf, + }); + + log::info!("[xdp-loader] receiver ready on '{}'", iface); + Box::into_raw(receiver) as *mut libc::c_void +} + +/// Blocking receive — reads next packet from ring buffer. +/// Returns payload length, or -1 on error, or -2 if closed. +/// +/// # Safety +/// `handle` must have been returned by `spoof_xdp_receiver_new`. +#[no_mangle] +pub unsafe extern "C" fn spoof_xdp_receiver_recv( + handle: *mut libc::c_void, + out_buf: *mut u8, + out_buf_len: usize, + src_ip_out: *mut u8, + src_port_out: *mut u16, +) -> i32 { + if handle.is_null() || out_buf.is_null() { + return -1; + } + + let receiver = &mut *(handle as *mut XdpReceiver); + let ring_raw_fd = receiver.ring.as_raw_fd(); + + loop { + if receiver.closed.load(Ordering::Relaxed) { + return -2; + } + + // Try to read from ring buffer + if let Some(item) = receiver.ring.next() { + let data = &*item; + let meta_size = std::mem::size_of::(); + + if data.len() < meta_size { + continue; + } + + let meta: PktMeta = std::ptr::read_unaligned(data.as_ptr() as *const PktMeta); + let payload_len = meta.payload_len as usize; + let payload_start = meta_size; + + if payload_start + payload_len > data.len() || payload_len > out_buf_len { + continue; + } + + // Copy payload + std::ptr::copy_nonoverlapping( + data[payload_start..payload_start + payload_len].as_ptr(), + out_buf, + payload_len, + ); + + if !src_ip_out.is_null() { + let ip_bytes = meta.src_ip.to_ne_bytes(); + std::ptr::copy_nonoverlapping(ip_bytes.as_ptr(), src_ip_out, 4); + } + if !src_port_out.is_null() { + *src_port_out = meta.src_port; + } + + return payload_len as i32; + } + + // No data — use epoll to wait efficiently (100ms timeout) + let epfd = libc::epoll_create1(0); + if epfd < 0 { + std::thread::sleep(std::time::Duration::from_micros(100)); + continue; + } + + let mut ev = libc::epoll_event { + events: libc::EPOLLIN as u32, + u64: 0, + }; + libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, ring_raw_fd, &mut ev); + + let mut events = [libc::epoll_event { events: 0, u64: 0 }; 1]; + libc::epoll_wait(epfd, events.as_mut_ptr(), 1, 100); + libc::close(epfd); + } +} + +/// Close the XDP receiver and detach the program. +#[no_mangle] +pub unsafe extern "C" fn spoof_xdp_receiver_close(handle: *mut libc::c_void) { + if handle.is_null() { + return; + } + let receiver = Box::from_raw(handle as *mut XdpReceiver); + receiver.closed.store(true, Ordering::SeqCst); + log::info!("[xdp-loader] detaching XDP from '{}'", receiver.interface); + drop(receiver); +} + +/// Check if XDP is available on this system (returns 1=yes, 0=no). +#[no_mangle] +pub extern "C" fn spoof_xdp_available() -> i32 { + let elf_data = aligned_elf_copy(); + match EbpfLoader::new().load(&elf_data) { + Ok(_) => 1, + Err(_) => 0, + } +}