From 5c1c202bfdfe30ae1a5c091ab41c5418baf93fc6 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 17 Jun 2026 21:57:46 +0000 Subject: [PATCH 01/10] core unify ebpf for linux --- build-linux.sh | 33 +++-- doc/plans/Innovation-4.2-core-unify-ebpf.md | 114 ++++++++++----- ebpf/socket.h | 58 +++++--- linux-ebpf/ebpf_cgroup.c | 110 +++++++------- linux-ebpf/socket.h | 134 +++++++---------- proxy_agent/build.rs | 151 ++++++++++++++++++++ shared-ebpf/include/gpa_audit_event.h | 76 ++++++++++ shared-ebpf/include/gpa_libbpf_helpers.h | 17 +++ 8 files changed, 483 insertions(+), 210 deletions(-) create mode 100644 shared-ebpf/include/gpa_audit_event.h create mode 100644 shared-ebpf/include/gpa_libbpf_helpers.h diff --git a/build-linux.sh b/build-linux.sh index ae4cf0fd..df54af3b 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -101,23 +101,12 @@ then exit $error_code fi -echo "======= build ebpf program after the proxy_agent_shared is built to let $out_dir created." -ebpf_path=$root_path/linux-ebpf - -if [ "$Target" == "arm64" ] -then - runthis clang -g -target bpf -Werror -O2 -D__TARGET_ARCH_arm64 -I/usr/include/aarch64-linux-gnu -c $ebpf_path/ebpf_cgroup.c -o $out_dir/ebpf_cgroup.o -else - runthis clang -g -target bpf -Werror -O2 -D__TARGET_ARCH_x86 -c $ebpf_path/ebpf_cgroup.c -o $out_dir/ebpf_cgroup.o -fi -error_code=$? -if [ $error_code -ne 0 ] -then - echo "call clang failed with exit-code: $error_code" - exit $error_code -fi -llvm-objdump -h $out_dir/ebpf_cgroup.o -ls -l $out_dir/ebpf_cgroup.o +echo "======= eBPF program is compiled by proxy_agent/build.rs during the cargo build below." +# proxy_agent/build.rs compiles linux-ebpf/ebpf_cgroup.c with CO-RE relocations +# for the selected target arch, errors out the build on any failure, and places +# the freshly compiled object directly at $out_dir/ebpf_cgroup.o (and its deps/ +# subdir) so we never pick up a stale copy from another build-output directory. +ebpf_obj_name=ebpf_cgroup.o echo "======= build proxy_agent" cargo_toml=$root_path/proxy_agent/Cargo.toml @@ -129,6 +118,16 @@ then echo "cargo build proxy_agent failed with exit-code: $error_code" exit $error_code fi + +echo "======= verify ebpf object produced by proxy_agent/build.rs" +if [ ! -f "$out_dir/$ebpf_obj_name" ] +then + echo "$out_dir/$ebpf_obj_name was not produced by proxy_agent/build.rs" + exit 1 +fi +llvm-objdump -h $out_dir/$ebpf_obj_name +ls -l $out_dir/$ebpf_obj_name + echo "======= copy config file for Linux platform" cp -f -T $root_path/proxy_agent/config/GuestProxyAgent.linux.json $out_dir/proxy-agent.json diff --git a/doc/plans/Innovation-4.2-core-unify-ebpf.md b/doc/plans/Innovation-4.2-core-unify-ebpf.md index a2fd30de..9e2d9510 100644 --- a/doc/plans/Innovation-4.2-core-unify-ebpf.md +++ b/doc/plans/Innovation-4.2-core-unify-ebpf.md @@ -17,10 +17,12 @@ Replace today's per-kernel-version eBPF source duplication with CO-RE (Compile Once, Run Everywhere). Share data structures between Linux (`linux-ebpf/`) and Windows (`ebpf/`), reduce build matrix to one object per program per platform. -**Files affected:** `linux-ebpf/`, `ebpf/`, `proxy_agent/build.rs`, `proxy_agent/src/redirector/`. +**Files affected:** `shared-ebpf/include/`, `linux-ebpf/`, `ebpf/`, `proxy_agent/build.rs`, `build-linux.sh`, `proxy_agent/src/redirector/`. > **Prerequisites:** None — foundational eBPF layer. Unblocks [4.1](Innovation-4.1-sk-lookup-bpf-lsm.md), [4.3](Innovation-4.3-ipv6-dual-stack.md), [4.4](Innovation-4.4-ebpf-throttling-lru.md), [5.1](Innovation-5.1-aks-container-native.md), [5.3](Innovation-5.3-cross-cloud-port.md). +> **Status (Linux):** ✅ Implemented. The Linux `cgroup/connect4` + `kprobe/tcp_v4_connect` program (`linux-ebpf/ebpf_cgroup.c`) now compiles to a single CO-RE object (`ebpf_cgroup.o`) that relocates kernel-struct field offsets at load time against the running kernel's BTF. Windows (`ebpf/`) is bridged (shared header referenced) but not yet fully migrated. + ## 1. Overview & Goals | Impact | Effort | Risk | Scope | @@ -39,65 +41,107 @@ Linux and Windows have two source trees with similar logic but separate definiti ## 3. CO-RE Design -- Adopt **libbpf** and **libbpf-rs** for the Linux side; use `BPF_CORE_READ()` for any kernel-struct dereference so the verifier relocates at load time. -- Use `vmlinux.h` generated from the build host's BTF as the canonical source for kernel-side types; relocations adapt to the target. -- For Windows, eBPF-for-Windows already uses BTF; mirror the same approach so user-space struct layouts align byte-for-byte. +**Implemented (Linux):** + +- The Linux loader is **aya** (`EbpfLoader::new().btf(Btf::from_sys_fs().ok().as_ref()).load_file(...)` in `proxy_agent/src/redirector/linux.rs`). Passing the kernel BTF from `/sys/kernel/btf/vmlinux` is what enables aya to apply CO-RE relocations at load time. +- Rather than pulling in a full generated `vmlinux.h`, `linux-ebpf/socket.h` declares **minimal kernel structs** (`struct sock`, `struct sock_common` — only the fields we read) inside a `#pragma clang attribute push(__attribute__((preserve_access_index)), apply_to = record)` block. `preserve_access_index` is the mechanism that turns each field access into a CO-RE relocation record (emitted into `.BTF`/`.BTF.ext` by `clang -g`). +- Kernel fields are read with `BPF_CORE_READ(sk, __sk_common.skc_daddr)` etc. into **plain local scalars**, e.g.: + + __u16 skc_family = BPF_CORE_READ(sk, __sk_common.skc_family); + __be32 skc_daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr); + __be16 skc_dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + __u16 skc_num = BPF_CORE_READ(sk, __sk_common.skc_num); + + This replaces the old `bpf_probe_read()` blob read against hardcoded offsets. + +**Two non-obvious requirements** (both caused load failures during bring-up): + +1. The kernel wrapper type **must be named `sock`** (the real kernel type name). A custom name such as `probe_sock` does not exist in the kernel BTF, so the ` ... struct probe_sock.__sk_common` relocation fails and the program is rejected at load. +2. The **destination of the read must be a plain (non-relocatable) type**. If you read into a local `struct sock_common` that also carries `preserve_access_index`, the write offset is relocated to the *kernel's* offset (e.g. `skc_family` at 16) and overflows the local stack copy — the verifier rejects it with `invalid write to stack`. + +- For Windows, eBPF-for-Windows already uses BTF; the plan is to mirror the same shared header so user-space struct layouts align byte-for-byte (bridge in place, full migration pending). ## 4. Shared Headers - // shared-ebpf/include/gpa_audit_event.h (consumed by both linux-ebpf and ebpf) +The canonical, platform-neutral structs live in `shared-ebpf/include/gpa_audit_event.h` and are consumed by `linux-ebpf/` today (and referenced by `ebpf/` for the Windows bridge). Their layouts are **binary-compatible with the Rust loader** in `proxy_agent/src/redirector/linux/ebpf_obj.rs`, which maps each to a fixed-size `[u32; N]` array — so field order/size cannot change without updating both sides. + + // shared-ebpf/include/gpa_audit_event.h #pragma once - #define GPA_AUDIT_PATH_MAX 256 - struct gpa_audit_event { - __u64 cgroup_id; - __u32 pid; - __u64 pid_starttime_ns; - __u32 uid; - __u32 gid; - __u32 measurement_kind; - __u8 measurement[32]; - char exe_path[GPA_AUDIT_PATH_MAX]; + + struct gpa_ip_address { // 16 B ([u32; 4]) + union { __u32 ipv4; __u32 ipv6[4]; }; + }; + struct gpa_destination_entry { // 24 B ([u32; 6]) + struct gpa_ip_address destination_ip; + __u32 destination_port; + __u32 protocol; + }; + struct gpa_audit_key { // 8 B ([u32; 2]) + __u32 protocol; + __u32 source_port; + }; + struct gpa_audit_event { // 20 B ([u32; 5]) + __u32 logon_id; // Linux: uid; Windows: logon_id + __u32 process_id; + __u32 is_root; // 1 if root/admin + __u32 destination_ipv4; + __u32 destination_port; }; - _Static_assert(sizeof(struct gpa_audit_event) == 320, "stable layout"); + struct gpa_skip_process_entry { __u32 pid; }; // 4 B ([u32; 1]) -- Rust bindings generated with `bindgen` in `build.rs`; the struct layout is asserted in CI on both platforms. +- The layout is locked at compile time with `_Static_assert(sizeof(...) == N, ...)` for each struct, so an accidental layout drift fails the eBPF build immediately instead of silently corrupting map data. +- `linux-ebpf/socket.h` provides backward-compatible `typedef`s (`destination_entry`, `sock_addr_audit_key`, `sock_addr_audit_entry`, ...) onto the `gpa_*` structs so existing code reads unchanged. +- The matching Rust layout is asserted by the `redirector::linux::ebpf_obj` unit tests (`destination_entry_test`, `sock_addr_audit_entry_test`, ...). ## 5. Build System -- `proxy_agent/build.rs` invokes `clang -target bpf -O2 -g -c` with `-emit-llvm`+`llc` for each program file. -- Pin `clang` version (15+); pin `llvm`. -- Generated `.bpf.o` files are checked into the artifact pipeline (not the repo). -- One output per program: `cgroup_connect.bpf.o`, `sk_lookup.bpf.o`, `lsm.bpf.o`, `audit_event.bpf.o`. +The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `cargo build`, making it the single source of truth for both `cargo` and the production script: + +- Invokes `clang -target bpf -Werror -O2 -g -I shared-ebpf/include -I linux-ebpf -c linux-ebpf/ebpf_cgroup.c -o ebpf_cgroup.o`. `-g` emits the `.BTF`/`.BTF.ext` sections that carry the CO-RE relocation records. +- **Arch-aware:** reads `CARGO_CFG_TARGET_ARCH` and selects `-D__TARGET_ARCH_x86` (x86_64) or `-D__TARGET_ARCH_arm64 -I/usr/include/aarch64-linux-gnu` (aarch64), mirroring the two branches that used to live in `build-linux.sh`. +- **Fails the build on any eBPF error** (`-Werror` + `panic!` on non-zero clang exit), so a broken program is never shipped. +- **Deterministic placement:** after compiling into `OUT_DIR`, the object is copied to the profile dir (e.g. `target/debug`, `out//release`) and its `deps/` subdir. This guarantees the runtime loader (current-exe dir) and the test harness always load the freshly compiled object instead of a stale copy from another `azure-proxy-agent-` build directory. +- `cargo:rerun-if-changed` on `shared-ebpf/include` and `linux-ebpf` recompiles whenever sources change. + +**`build-linux.sh`** no longer compiles eBPF itself: it adds the shared include path, lets `build.rs` produce `ebpf_cgroup.o`, then verifies the object exists at `$out_dir/ebpf_cgroup.o` before packaging. + +- Output name is `ebpf_cgroup.o`, matching `ebpfProgramName` in `proxy_agent/config/GuestProxyAgent.linux.json`. (Future programs — `sk_lookup.o`, `lsm.o` — slot into the `ebpf_programs` list in `build.rs`.) +- Requires `clang` 15+. ## 6. BTF Strategy -- Prefer kernel-provided `/sys/kernel/btf/vmlinux` at load time. -- For kernels without BTF, ship a small BTF stub generated by `pahole` for the structs we actually use (`struct sock`, `struct task_struct`'s relevant fields). -- Document a minimum kernel of 5.4 (per current README compatibility) with CO-RE relocations rather than per-kernel builds. +- At load time aya reads the kernel-provided `/sys/kernel/btf/vmlinux` (`Btf::from_sys_fs()`) and relocates the program's field accesses to the running kernel's layout. +- We deliberately **do not** ship or commit a generated `vmlinux.h`. The program only touches a handful of `struct sock_common` fields, so minimal hand-declared structs with `preserve_access_index` cover the relocation set and keep the source small. (A generated `vmlinux.h` remains the fallback if future programs touch many kernel structs.) +- Minimum kernel target remains 5.4 (per README), served by CO-RE relocations rather than per-kernel builds. ## 7. Integration -- `proxy_agent/src/redirector/linux/` uses `libbpf-rs` Skel objects generated by `libbpf-cargo`. -- Windows side switches to consume the same header for the user-space event ring buffer. -- Removed: per-distro compile-time forks; replaced with feature probes at load. +- `proxy_agent/src/redirector/linux.rs` loads the object with **aya** (`EbpfLoader` + BTF) and attaches the two programs by name: `connect4` (`CgroupSockAddr`) and `tcp_v4_connect` (`KProbe` on `tcp_connect`). +- `proxy_agent/src/redirector/linux/ebpf_obj.rs` keeps the `[u32; N]` ↔ `gpa_*` binary contract for map keys/values. +- Windows side references the shared header for its user-space event layout (bridge); full switch to the shared structs is pending. +- Removed: hardcoded-offset blob read of `sock_common`; replaced with per-field CO-RE reads. ## 8. Tests -- Cross-kernel matrix CI (5.4, 5.15, 6.1, 6.8) loads each program and runs a smoke test that triggers the audit event. -- Layout assertion test on both Linux and Windows; deliberate field add must update version number. -- Performance: load time \< 100 ms across all kernels. +- `redirector::linux::ebpf_obj` layout unit tests assert the Rust `[u32; N]` mappings match the shared C structs (run on every build). +- `redirector::linux::tests::linux_ebpf_test` (feature `test-with-root`, run by `build-linux.sh`) actually **loads and attaches** the CO-RE object on the build host, exercising the kprobe relocation path end-to-end. Bring-up validated it loads cleanly via `bpftool prog loadall` (no unresolved CO-RE relocations, verifier accepts the program). +- Pending: cross-kernel matrix CI (5.4, 5.15, 6.1, 6.8) loading the same object; load-time budget \< 100 ms; Windows layout assertion. ## 9. Risks -- **clang/LLVM bugs** in CO-RE relocation. Mitigation: pin known-good toolchain; fallback to per-kernel build path retained for one release. -- **vmlinux.h size** — large but only at build time; not shipped. +- **CO-RE relocation name mismatch** — kernel wrapper types must use real kernel names (`sock`, not `probe_sock`) or relocations fail at load. Mitigation: covered by the `linux_ebpf_test` load test. +- **Relocating a local copy** — reading into a `preserve_access_index` type overflows the stack copy; always read CO-RE fields into plain scalars. Mitigation: documented in `ebpf_cgroup.c`; load test catches regressions. +- **clang/LLVM bugs** in CO-RE relocation. Mitigation: pin clang 15+. +- **Stale object pickup** — multiple build-output dirs can leave old objects around. Mitigation: `build.rs` writes the object to a deterministic profile/`deps` path; `build-linux.sh` verifies it. ## 10. Milestones | M | Deliverable | Exit | |-----|----------------------------------------------|------------------------------------------------------| -| M1 | libbpf-rs adoption | Existing program runs unchanged on supported kernels | -| M2 | Shared header + cross-platform Rust bindings | Layout assertions green on both OSes | -| M3 | Drop per-kernel builds | CI matrix \< 1/3 of previous count | +| M1 | Linux CO-RE object via aya + preserve_access_index | ✅ `ebpf_cgroup.o` loads/attaches on supported kernels | +| M2 | Shared header + Rust layout assertions | ✅ `ebpf_obj` layout tests green on Linux | +| M3 | Arch-aware, fail-fast, deterministic build | ✅ `build.rs` (x86_64 + arm64) owns the compile | +| M4 | Windows migration to shared structs | Bridge in place; full switch pending | +| M5 | Drop per-kernel builds + cross-kernel CI | CI matrix \< 1/3 of previous count | Detail design for direction 4.2. Parent: [Innovation-Directions.md](Innovation-Directions.md). diff --git a/ebpf/socket.h b/ebpf/socket.h index 595b3c5f..9c012a88 100644 --- a/ebpf/socket.h +++ b/ebpf/socket.h @@ -1,46 +1,62 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT +// +// Windows eBPF socket definitions +// Uses shared audit event structures from gpa_audit_event.h +// This header bridges Windows typedefs (uint32_t) with shared kernel definitions (__u32) #pragma once #include #include +// Standard protocol constants #define IPPROTO_TCP 6 #define IPPROTO_UDP 17 #define AF_INET 2 #define AF_INET6 0x17 -typedef struct _ip_address -{ - union - { - uint32_t ipv4; - uint32_t ipv6[4]; - }; -} ip_address_t; +// ============================================================================ +// MIGRATION IN PROGRESS: These typedefs bridge to shared structures +// See ../shared-ebpf/include/gpa_audit_event.h for canonical definitions +// ============================================================================ +// Destination entry for policy routing +// Note: In CO-RE, this will be unified with Linux version typedef struct _destination_entry { - ip_address_t destination_ip; + union { + uint32_t ipv4; + uint32_t ipv6[4]; + } destination_ip; uint16_t destination_port; uint32_t protocol; } destination_entry_t; -typedef struct _sock_addr_audit_key{ +// Audit key for efficient map lookups +// Replaces: struct gpa_audit_key (from shared header) +typedef struct _sock_addr_audit_key +{ uint32_t protocol; uint16_t source_port; -}sock_addr_audit_key_t; - -typedef struct _sock_addr_audit_entry{ - uint64_t logon_id; - uint32_t process_id; - int32_t is_admin; - uint32_t destination_ipv4; - uint16_t destination_port; -}sock_addr_audit_entry_t; +} sock_addr_audit_key_t; -typedef struct _sock_addr_skip_process_entry{ +// Audit entry - IMPORTANT: This is being unified with gpa_audit_event +// The canonical structure is now at ../shared-ebpf/include/gpa_audit_event.h +// TODO: Migrate to use the shared struct gpa_audit_event when Windows eBPF +// updates its event ring buffer handling to match Linux/shared layout +typedef struct _sock_addr_audit_entry +{ + uint64_t logon_id; // Will map to uid in shared struct + uint32_t process_id; // Same as pid in shared struct + int32_t is_admin; // Same as is_admin in shared struct + uint32_t destination_ipv4; // Same in shared struct + uint16_t destination_port; // Same in shared struct +} sock_addr_audit_entry_t; + +// Skip process entry +typedef struct _sock_addr_skip_process_entry +{ uint32_t pid; -}sock_addr_skip_process_entry; \ No newline at end of file +} sock_addr_skip_process_entry; \ No newline at end of file diff --git a/linux-ebpf/ebpf_cgroup.c b/linux-ebpf/ebpf_cgroup.c index bc617a91..a58f82f7 100644 --- a/linux-ebpf/ebpf_cgroup.c +++ b/linux-ebpf/ebpf_cgroup.c @@ -1,42 +1,44 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT +// +// eBPF cgroup/connect4 program for connection interception and audit +// Uses CO-RE (Compile Once, Run Everywhere) for kernel compatibility + #include #include #include #include +#include #include "socket.h" -struct -{ +// BPF maps for policy and audit +struct { __uint(type, BPF_MAP_TYPE_HASH); - __type(key, sock_addr_skip_process_entry); - __type(value, sock_addr_skip_process_entry); + __type(key, struct gpa_skip_process_entry); + __type(value, struct gpa_skip_process_entry); __uint(max_entries, 10); } skip_process_map SEC(".maps"); -struct -{ +struct { __uint(type, BPF_MAP_TYPE_HASH); - __type(key, destination_entry); - __type(value, destination_entry); + __type(key, struct gpa_destination_entry); + __type(value, struct gpa_destination_entry); __uint(max_entries, 10); } policy_map SEC(".maps"); -struct -{ +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); - __type(key, sock_addr_audit_key); // source port and protocol - __type(value, sock_addr_audit_entry); // audit entry - __uint(max_entries, 200); // some older kernel version cannot support over 200 entries. + __type(key, struct gpa_audit_key); // source port and protocol + __type(value, struct gpa_audit_event); // audit event (canonical struct) + __uint(max_entries, 200); // LRU evicts oldest on overflow } audit_map SEC(".maps"); -struct -{ +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); - __type(key, __u64); // socket cookie or pid-tgid - __type(value, sock_addr_local_entry); // audit local entry - __uint(max_entries, 200); // some older kernel version cannot support over 200 entries. + __type(key, __u64); // pid-tgid or socket cookie + __type(value, struct gpa_sock_addr_local_entry); + __uint(max_entries, 200); } local_map SEC(".maps"); @@ -47,11 +49,11 @@ struct static __always_inline int check_skip_process_map_entry(__u32 pid) { - sock_addr_skip_process_entry key = {0}; + struct gpa_skip_process_entry key = {0}; key.pid = pid; // Find the entry in the skip_process map. - sock_addr_skip_process_entry *skip_entry = bpf_map_lookup_elem(&skip_process_map, &key); + struct gpa_skip_process_entry *skip_entry = bpf_map_lookup_elem(&skip_process_map, &key); return (skip_entry != NULL) ? 1 : 0; } @@ -71,7 +73,7 @@ update_local_map_entry(struct bpf_sock_addr *ctx) return 1; } - sock_addr_local_entry entry = {0}; + struct gpa_sock_addr_local_entry entry = {0}; entry.process_id = pid; __u32 uid = (__u32)(bpf_get_current_uid_gid() >> 32); entry.logon_id = uid; @@ -96,13 +98,13 @@ update_local_map_entry(struct bpf_sock_addr *ctx) static __always_inline int authorize_v4(struct bpf_sock_addr *ctx) { - destination_entry entry = {0}; + struct gpa_destination_entry entry = {0}; entry.destination_ip.ipv4 = ctx->user_ip4; entry.destination_port = ctx->user_port; entry.protocol = ctx->protocol; // Find the entry in the policy map. - destination_entry *policy = bpf_map_lookup_elem(&policy_map, &entry); + struct gpa_destination_entry *policy = bpf_map_lookup_elem(&policy_map, &entry); if (policy != NULL) { bpf_printk("authorize_v4: Found v4 proxy entry value: %u, %u", policy->destination_ip.ipv4, policy->destination_port); @@ -142,13 +144,13 @@ int connect4(struct bpf_sock_addr *ctx) } static __always_inline int -update_audit_map_entry_sk(__u32 local_port, sock_addr_local_entry *local_entry) +update_audit_map_entry_sk(__u32 local_port, struct gpa_sock_addr_local_entry *local_entry) { - sock_addr_audit_key key = {0}; + struct gpa_audit_key key = {0}; key.protocol = local_entry->protocol; key.source_port = local_port; - sock_addr_audit_entry entry = {0}; + struct gpa_audit_event entry = {0}; entry.process_id = local_entry->process_id; entry.logon_id = local_entry->logon_id; entry.is_root = local_entry->is_root; @@ -169,22 +171,24 @@ update_audit_map_entry_sk(__u32 local_port, sock_addr_local_entry *local_entry) } static __always_inline int -trace_v4(struct pt_regs *ctx, struct probe_sock *sk) +trace_v4(struct pt_regs *ctx, struct sock *sk) { - struct sock_common skc; - // bpf_probe_read_kernel helper function requires kernel version 5.5+ - // hence have to use bpf_probe_read helper function instead. - long re = bpf_probe_read(&skc, sizeof(struct sock_common), &sk->__sk_common); - if (re != 0) - { - // 0 is success. - return 0; - } - if (skc.skc_family != AF_INET) + // CO-RE relocatable reads of kernel struct sock fields. + // BPF_CORE_READ relocates each field offset on the KERNEL-side type + // (struct sock, which carries preserve_access_index in socket.h) to the + // running kernel's layout at load time. The destinations below are plain + // local scalars (no preserve_access_index), so their offsets are NOT + // relocated - this is required, otherwise the verifier rejects writes that + // would land outside our local stack copy. + __u16 skc_family = BPF_CORE_READ(sk, __sk_common.skc_family); + if (skc_family != AF_INET) { // Only support IPv4. return 0; } + __be32 skc_daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr); + __be16 skc_dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + __u16 skc_num = BPF_CORE_READ(sk, __sk_common.skc_num); __u64 pid_tgid = bpf_get_current_pid_tgid(); __u32 pid = (__u32)(pid_tgid >> 32); @@ -195,10 +199,10 @@ trace_v4(struct pt_regs *ctx, struct probe_sock *sk) } // Find the entry in the local map. - sock_addr_local_entry *local_entry = bpf_map_lookup_elem(&local_map, &pid_tgid); + struct gpa_sock_addr_local_entry *local_entry = bpf_map_lookup_elem(&local_map, &pid_tgid); if (local_entry != NULL) { - update_audit_map_entry_sk(skc.skc_num, local_entry); + update_audit_map_entry_sk(skc_num, local_entry); __u64 ret = bpf_map_delete_elem(&local_map, &pid_tgid); if (ret != 0) { @@ -211,27 +215,27 @@ trace_v4(struct pt_regs *ctx, struct probe_sock *sk) return 0; } - destination_entry entry = {0}; - entry.destination_ip.ipv4 = skc.skc_daddr; - entry.destination_port = skc.skc_dport; + struct gpa_destination_entry entry = {0}; + entry.destination_ip.ipv4 = skc_daddr; + entry.destination_port = skc_dport; entry.protocol = IPPROTO_TCP; // Find the entry in the policy map. - destination_entry *policy = bpf_map_lookup_elem(&policy_map, &entry); + struct gpa_destination_entry *policy = bpf_map_lookup_elem(&policy_map, &entry); if (policy != NULL) { __u32 uid = (__u32)(bpf_get_current_uid_gid() >> 32); - sock_addr_audit_key key = {0}; + struct gpa_audit_key key = {0}; key.protocol = IPPROTO_TCP; - key.source_port = skc.skc_num; + key.source_port = skc_num; - sock_addr_audit_entry entry = {0}; - entry.process_id = pid; - entry.logon_id = uid; - entry.is_root = (uid == 0) ? 1 : 0; // root uid is 0. - entry.destination_ipv4 = skc.skc_daddr; - entry.destination_port = skc.skc_dport; + struct gpa_audit_event audit_entry = {0}; + audit_entry.process_id = pid; + audit_entry.logon_id = uid; + audit_entry.is_root = (uid == 0) ? 1 : 0; // root uid is 0. + audit_entry.destination_ipv4 = skc_daddr; + audit_entry.destination_port = skc_dport; - __u64 ret = bpf_map_update_elem(&audit_map, &key, &entry, 0); + __u64 ret = bpf_map_update_elem(&audit_map, &key, &audit_entry, 0); if (ret != 0) { bpf_printk("trace_v4: Failed to update audit map entry with results:%u.", ret); @@ -246,7 +250,7 @@ trace_v4(struct pt_regs *ctx, struct probe_sock *sk) } SEC("kprobe/tcp_v4_connect") -int BPF_KPROBE(tcp_v4_connect, struct probe_sock *sk) +int BPF_KPROBE(tcp_v4_connect, struct sock *sk) { return trace_v4(ctx, sk); } diff --git a/linux-ebpf/socket.h b/linux-ebpf/socket.h index 99b5015d..527e450d 100644 --- a/linux-ebpf/socket.h +++ b/linux-ebpf/socket.h @@ -1,45 +1,31 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT -#define BPF_SOCK_ADDR_VERDICT_PROCEED 1 -#define IPPROTO_TCP 6 -#define AF_INET 2 +// +// Linux-specific eBPF helpers and definitions +// Includes shared audit event structures from gpa_audit_event.h -typedef struct _sock_addr_skip_process_entry -{ - __u32 pid; -} sock_addr_skip_process_entry; +#pragma once -typedef struct _ip_address -{ - union - { - __u32 ipv4; - __u32 ipv6[4]; - }; -} ip_address; +#include "../shared-ebpf/include/gpa_audit_event.h" +#include "../shared-ebpf/include/gpa_libbpf_helpers.h" -typedef struct _destination_entry -{ - ip_address destination_ip; - __u32 destination_port; - __u32 protocol; -} destination_entry; +// Linux-specific verdicts +#define BPF_SOCK_ADDR_VERDICT_PROCEED 1 -typedef struct _sock_addr_audit_key -{ - __u32 protocol; - __u32 source_port; -} sock_addr_audit_key; +// Standard protocol constants +#define IPPROTO_TCP 6 +#define IPPROTO_UDP 17 +#define AF_INET 2 +#define AF_INET6 10 -typedef struct _sock_addr_audit_entry -{ - __u32 logon_id; - __u32 process_id; - __u32 is_root; - __u32 destination_ipv4; - __u32 destination_port; -} sock_addr_audit_entry; +// Type aliases for backward compatibility with existing code +typedef struct gpa_skip_process_entry sock_addr_skip_process_entry; +typedef struct gpa_destination_entry destination_entry; +typedef struct gpa_audit_key sock_addr_audit_key; +typedef struct gpa_audit_event sock_addr_audit_entry; +typedef struct gpa_sock_addr_local_entry sock_addr_local_entry; +// IPv4 socket tuple (used for connection tracking) typedef struct _bpf_sock_tuple_ipv4 { __be32 saddr; @@ -48,66 +34,46 @@ typedef struct _bpf_sock_tuple_ipv4 __be16 dport; } bpf_sock_tuple_ipv4; -typedef struct _sock_addr_local_entry -{ - __u32 logon_id; - __u32 process_id; - __u32 is_root; - __u32 destination_ipv4; - __u32 destination_port; - __u32 protocol; -} sock_addr_local_entry; +// ============================================================================ +// CO-RE kernel struct definitions +// ============================================================================ +// These minimal kernel struct definitions are marked with +// __attribute__((preserve_access_index)) which is the CORE of CO-RE: +// instead of hardcoding field offsets at compile time, the BPF loader +// (libbpf/aya) relocates each field access to the TARGET kernel's actual +// offset at load time, using the kernel's BTF (/sys/kernel/btf/vmlinux). +// +// This is what makes "Compile Once, Run Everywhere" work: the same .bpf.o +// adapts to different kernel versions automatically. -typedef __u32 __bitwise __portpair; -typedef __u64 __bitwise __addrpair; +#pragma clang attribute push(__attribute__((preserve_access_index)), apply_to = record) -struct hlist_node -{ - struct hlist_node *next, **pprev; -}; - -struct sock_common -{ - union - { - __addrpair skc_addrpair; - struct - { +// Minimal sock_common - only the fields we actually read. +// Field offsets are relocated by CO-RE; we only need the names to match +// the kernel's struct sock_common (verified against vmlinux BTF). +struct sock_common { + union { + struct { __be32 skc_daddr; __be32 skc_rcv_saddr; }; }; - union - { - unsigned int skc_hash; - __u16 skc_u16hashes[2]; - }; - /* skc_dport && skc_num must be grouped as well */ - union - { - __portpair skc_portpair; - struct - { + union { + struct { __be16 skc_dport; __u16 skc_num; }; }; - - unsigned short skc_family; - volatile unsigned char skc_state; - unsigned char skc_reuse : 4; - unsigned char skc_reuseport : 1; - unsigned char skc_ipv6only : 1; - unsigned char skc_net_refcnt : 1; - int skc_bound_dev_if; - union - { - struct hlist_node skc_bind_node; - struct hlist_node skc_portaddr_node; - }; + short unsigned int skc_family; }; -struct probe_sock -{ +// Minimal sock wrapper - we only access __sk_common. +// IMPORTANT: this must be named exactly "sock" (the kernel's type name) so the +// CO-RE relocation against &sk->__sk_common resolves to struct sock in the +// target kernel's BTF. A custom name (e.g. probe_sock) does not exist in +// kernel BTF and makes the program fail to load. +struct sock { struct sock_common __sk_common; -}; \ No newline at end of file +}; + +#pragma clang attribute pop \ No newline at end of file diff --git a/proxy_agent/build.rs b/proxy_agent/build.rs index cb5f52ce..ce87970d 100644 --- a/proxy_agent/build.rs +++ b/proxy_agent/build.rs @@ -1,10 +1,161 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT +use std::env; +use std::path::PathBuf; +use std::process::Command; + fn main() { + // Windows build setup #[cfg(windows)] { static_vcruntime::metabuild(); let res = winres::WindowsResource::new(); res.compile().unwrap(); } + + // Linux eBPF compilation with CO-RE support + #[cfg(not(windows))] + { + compile_ebpf_with_core(); + } +} + +#[cfg(not(windows))] +fn compile_ebpf_with_core() { + let out_dir = env::var("OUT_DIR").unwrap(); + let out_path = PathBuf::from(&out_dir); + let workspace_root = env::var("CARGO_MANIFEST_DIR") + .map(|d| PathBuf::from(d).parent().unwrap().to_path_buf()) + .expect("Failed to determine workspace root"); + + // Re-run this build script when any eBPF source or shared header changes. + println!( + "cargo:rerun-if-changed={}/shared-ebpf/include", + workspace_root.display() + ); + println!( + "cargo:rerun-if-changed={}/linux-ebpf", + workspace_root.display() + ); + + // Select the architecture-specific defines/includes based on the cargo + // build target so we produce a correct object for both x86_64 and arm64. + // This mirrors the arm64/x86 branches in build-linux.sh. + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| "x86_64".to_string()); + let (arch_define, arch_include) = match target_arch.as_str() { + "aarch64" => ( + "-D__TARGET_ARCH_arm64", + Some("-I/usr/include/aarch64-linux-gnu".to_string()), + ), + _ => ("-D__TARGET_ARCH_x86", None), + }; + + // Build flags for CO-RE compilation. These mirror build-linux.sh so both + // paths produce identical, portable objects. + // -g emits BTF debug info; -target bpf + clang relocations produce a + // portable object whose kernel-struct field offsets are resolved at load + // time against the target kernel's BTF (CO-RE). + // -Werror makes any eBPF compile warning fail the build (matches build-linux.sh). + let core_flags = vec![ + "-target".to_string(), + "bpf".to_string(), + "-Werror".to_string(), + "-O2".to_string(), + "-g".to_string(), + arch_define.to_string(), + ]; + + // Include paths + let mut include_paths = vec![ + format!("-I{}/shared-ebpf/include", workspace_root.display()), + format!("-I{}/linux-ebpf", workspace_root.display()), + ]; + if let Some(inc) = arch_include { + include_paths.push(inc); + } + + // eBPF programs to compile (CO-RE compatible). + // Output name matches the runtime config (`ebpfProgramName`) so the loader + // in src/redirector.rs can find it. This mirrors what build-linux.sh + // produces, but with CO-RE relocations enabled. + let ebpf_programs = vec![ + ("linux-ebpf/ebpf_cgroup.c", "ebpf_cgroup.o"), + // Add more programs here as they're converted to CO-RE: + // ("linux-ebpf/sk_lookup.c", "sk_lookup.o"), + // ("linux-ebpf/lsm.c", "lsm.o"), + ]; + + for (src, out_obj) in ebpf_programs { + let src_path = workspace_root.join(src); + let obj_path = out_path.join(out_obj); + + if !src_path.exists() { + panic!("eBPF source not found: {}", src_path.display()); + } + + // Compile with clang (requires clang-15+) + let mut cmd = Command::new("clang"); + cmd.args(&core_flags) + .args(&include_paths) + .arg("-c") + .arg(&src_path) + .arg("-o") + .arg(&obj_path); + + match cmd.output() { + Ok(output) => { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + // Fail the cargo build so a broken eBPF program is never shipped. + panic!("Failed to compile {src}:\n{stderr}"); + } else { + println!("cargo:warning=Compiled {} -> {}", src, obj_path.display()); + // Also place the freshly compiled object at a deterministic + // location next to the built binaries so the runtime loader + // (get_ebpf_file_path -> current exe dir) and the test + // harness always pick up THIS object, not a stale copy left + // in some other build-script-output hash directory. + // OUT_DIR = ///build//out + // so three parents up is the profile dir (e.g. target/debug). + if let Some(profile_dir) = out_path + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + { + copy_obj_to(&obj_path, &profile_dir.join(out_obj)); + let deps_dir = profile_dir.join("deps"); + if deps_dir.is_dir() { + copy_obj_to(&obj_path, &deps_dir.join(out_obj)); + } + } + } + } + Err(e) => { + panic!("Error running clang for {src}: {e}. Make sure clang-15+ is installed."); + } + } + } + + // Report whether kernel BTF is available on the build host. At runtime the + // aya loader reads /sys/kernel/btf/vmlinux to perform CO-RE relocations. + let vmlinux_btf = PathBuf::from("/sys/kernel/btf/vmlinux"); + if !vmlinux_btf.exists() { + println!( + "cargo:warning=vmlinux BTF not found at {:?}; CO-RE relocations require kernel BTF at load time", + vmlinux_btf + ); + } +} + +#[cfg(not(windows))] +fn copy_obj_to(src: &std::path::Path, dst: &std::path::Path) { + if let Err(e) = std::fs::copy(src, dst) { + // Non-fatal: the canonical object still exists in OUT_DIR. Warn so a + // stale copy is never silently relied upon. + println!( + "cargo:warning=failed to copy {} -> {}: {e}", + src.display(), + dst.display() + ); + } } diff --git a/shared-ebpf/include/gpa_audit_event.h b/shared-ebpf/include/gpa_audit_event.h new file mode 100644 index 00000000..bbeb082f --- /dev/null +++ b/shared-ebpf/include/gpa_audit_event.h @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MIT +// +// Shared eBPF header for audit events - used by both Linux and Windows eBPF programs +// This header defines the canonical, platform-neutral struct layout for audit data +// that flows between kernel eBPF programs and user-space policy/audit handlers. +// +// IMPORTANT: These layouts are binary-compatible with the Rust loader in +// proxy_agent/src/redirector/linux/ebpf_obj.rs. The Rust side maps these to +// fixed-size [u32; N] arrays, so field order and sizes MUST NOT change without +// updating both sides simultaneously. + +#pragma once + +// IP address - union allows IPv4 (first element) or IPv6 (all 4 elements) +// Size: 16 bytes (4 x u32) - matches Rust _ip_address { ip: [u32; 4] } +struct gpa_ip_address { + union { + __u32 ipv4; + __u32 ipv6[4]; + }; +}; + +// Policy destination entry - defines where traffic should be redirected +// Size: 24 bytes - matches Rust destination_entry -> [u32; 6] +// ip_address(16) + destination_port(4) + protocol(4) +struct gpa_destination_entry { + struct gpa_ip_address destination_ip; + __u32 destination_port; // Port stored as u32 (network byte order in lower 16 bits) + __u32 protocol; // IPPROTO_TCP (6) or IPPROTO_UDP (17) +}; + +// Minimal audit key for map lookups (protocol + source port) +// Size: 8 bytes - matches Rust sock_addr_audit_key -> [u32; 2] +struct gpa_audit_key { + __u32 protocol; // IPPROTO_TCP or IPPROTO_UDP + __u32 source_port; // Local source port (stored as u32) +}; + +// Canonical audit event entry - the record stored in the audit map +// Size: 20 bytes - matches Rust sock_addr_audit_entry -> [u32; 5] +// +// NOTE: Field names use Linux semantics (logon_id=uid, is_root). The Windows +// side maps these to its own naming (logon_id, is_admin) at user-space. +// This is the CANONICAL shared struct - both platforms agree on this layout. +struct gpa_audit_event { + __u32 logon_id; // Linux: uid; Windows: logon_id (lower 32 bits) + __u32 process_id; // Process ID + __u32 is_root; // 1 if root/admin, 0 otherwise + __u32 destination_ipv4; // Destination IPv4 address + __u32 destination_port; // Destination port (stored as u32) +}; + +// Skip process entry - processes in this map bypass audit/redirect +// Size: 4 bytes - matches Rust sock_addr_skip_process_entry -> [u32; 1] +struct gpa_skip_process_entry { + __u32 pid; +}; + +// Local address entry - tracks current connection state in the local_map +// Size: 24 bytes (6 x u32) +struct gpa_sock_addr_local_entry { + __u32 logon_id; // uid + __u32 process_id; + __u32 is_root; + __u32 destination_ipv4; + __u32 destination_port; + __u32 protocol; +}; + +// Compile-time layout assertions to guarantee binary compatibility with Rust loader. +// If these fail, the Rust [u32; N] mappings in ebpf_obj.rs must be updated too. +_Static_assert(sizeof(struct gpa_destination_entry) == 24, "destination_entry must be 24 bytes ([u32; 6])"); +_Static_assert(sizeof(struct gpa_audit_key) == 8, "audit_key must be 8 bytes ([u32; 2])"); +_Static_assert(sizeof(struct gpa_audit_event) == 20, "audit_event must be 20 bytes ([u32; 5])"); +_Static_assert(sizeof(struct gpa_skip_process_entry) == 4, "skip_process_entry must be 4 bytes ([u32; 1])"); diff --git a/shared-ebpf/include/gpa_libbpf_helpers.h b/shared-ebpf/include/gpa_libbpf_helpers.h new file mode 100644 index 00000000..51530175 --- /dev/null +++ b/shared-ebpf/include/gpa_libbpf_helpers.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MIT +// +// Shared libbpf helpers and macros for portable eBPF code +// Provides unified interface for both Linux and Windows eBPF programs +// +// For CO-RE kernel struct access, include directly and +// use bpf_core_read() / BPF_CORE_READ(). Those macros rely on the +// preserve_access_index attribute applied to kernel structs (see socket.h), +// which lets the loader relocate field offsets to the target kernel at load time. + +#pragma once + +#include +#include + +// EOF From 0dc4d365edf55874e48253fc5dd4998a97d0755b Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 17 Jun 2026 22:18:19 +0000 Subject: [PATCH 02/10] update use for linux only --- proxy_agent/build.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/proxy_agent/build.rs b/proxy_agent/build.rs index ce87970d..0898fdf4 100644 --- a/proxy_agent/build.rs +++ b/proxy_agent/build.rs @@ -1,8 +1,5 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT -use std::env; -use std::path::PathBuf; -use std::process::Command; fn main() { // Windows build setup @@ -22,6 +19,10 @@ fn main() { #[cfg(not(windows))] fn compile_ebpf_with_core() { + use std::env; + use std::path::PathBuf; + use std::process::Command; + let out_dir = env::var("OUT_DIR").unwrap(); let out_path = PathBuf::from(&out_dir); let workspace_root = env::var("CARGO_MANIFEST_DIR") From 316a742e585448ffc63dad37a917f241023a0f81 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 17 Jun 2026 22:57:20 +0000 Subject: [PATCH 03/10] os_string_as_string at serde --- proxy_agent/src/proxy.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/proxy_agent/src/proxy.rs b/proxy_agent/src/proxy.rs index b0be815c..0de7349c 100644 --- a/proxy_agent/src/proxy.rs +++ b/proxy_agent/src/proxy.rs @@ -53,6 +53,7 @@ pub struct Claims { pub userName: String, pub userGroups: Vec, pub processId: u32, + #[serde(with = "os_string_as_string")] pub processName: OsString, pub processFullPath: PathBuf, pub processCmdLine: String, @@ -61,6 +62,29 @@ pub struct Claims { pub clientPort: u16, } +/// Serialize/deserialize an `OsString` as a human-readable UTF-8 string instead +/// of serde's default platform-tagged byte array (e.g. `{"Unix":[112,121,...]}` +/// or `{"Windows":[87,105,...]}`). Non-UTF-8 bytes are replaced lossily. +mod os_string_as_string { + use serde::{Deserialize, Deserializer, Serializer}; + use std::ffi::OsString; + + pub fn serialize(value: &OsString, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string_lossy()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(OsString::from(s)) + } +} + struct Process { pub command_line: String, pub name: OsString, From 8fe77ddf5f421881d0067471a562abe7094920f6 Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Wed, 17 Jun 2026 16:03:03 -0700 Subject: [PATCH 04/10] windows build --- build.cmd | 1 + doc/plans/Innovation-4.2-core-unify-ebpf.md | 15 ++-- ebpf/redirect.bpf.c | 13 ++- ebpf/socket.h | 48 ++-------- proxy_agent/src/redirector/windows.rs | 87 +++++++++++++++---- proxy_agent/src/redirector/windows/bpf_obj.rs | 59 +++++++++++-- .../src/redirector/windows/bpf_prog.rs | 49 +++++++++-- 7 files changed, 189 insertions(+), 83 deletions(-) diff --git a/build.cmd b/build.cmd index 72d0640b..ab09cc87 100644 --- a/build.cmd +++ b/build.cmd @@ -155,6 +155,7 @@ if %ERRORLEVEL% NEQ 0 ( echo call cargo build proxy_agent failed with exit-code: %errorlevel% exit /b %errorlevel% ) + if "%Target%"=="arm64" ( echo ======= skip running proxy_agent arm64 tests on amd64 machine ) else ( diff --git a/doc/plans/Innovation-4.2-core-unify-ebpf.md b/doc/plans/Innovation-4.2-core-unify-ebpf.md index 9e2d9510..33bb7031 100644 --- a/doc/plans/Innovation-4.2-core-unify-ebpf.md +++ b/doc/plans/Innovation-4.2-core-unify-ebpf.md @@ -21,7 +21,7 @@ Replace today's per-kernel-version eBPF source duplication with CO-RE (Compile O > **Prerequisites:** None — foundational eBPF layer. Unblocks [4.1](Innovation-4.1-sk-lookup-bpf-lsm.md), [4.3](Innovation-4.3-ipv6-dual-stack.md), [4.4](Innovation-4.4-ebpf-throttling-lru.md), [5.1](Innovation-5.1-aks-container-native.md), [5.3](Innovation-5.3-cross-cloud-port.md). -> **Status (Linux):** ✅ Implemented. The Linux `cgroup/connect4` + `kprobe/tcp_v4_connect` program (`linux-ebpf/ebpf_cgroup.c`) now compiles to a single CO-RE object (`ebpf_cgroup.o`) that relocates kernel-struct field offsets at load time against the running kernel's BTF. Windows (`ebpf/`) is bridged (shared header referenced) but not yet fully migrated. +> **Status (Linux/Windows):** ✅ Implemented for Linux CO-RE. ✅ Windows now uses shared audit structs in `ebpf/` and keeps runtime compatibility with both new and legacy Windows eBPF audit layouts during mixed-version rollout. ## 1. Overview & Goals @@ -59,7 +59,7 @@ Linux and Windows have two source trees with similar logic but separate definiti 1. The kernel wrapper type **must be named `sock`** (the real kernel type name). A custom name such as `probe_sock` does not exist in the kernel BTF, so the ` ... struct probe_sock.__sk_common` relocation fails and the program is rejected at load. 2. The **destination of the read must be a plain (non-relocatable) type**. If you read into a local `struct sock_common` that also carries `preserve_access_index`, the write offset is relocated to the *kernel's* offset (e.g. `skc_family` at 16) and overflows the local stack copy — the verifier rejects it with `invalid write to stack`. -- For Windows, eBPF-for-Windows already uses BTF; the plan is to mirror the same shared header so user-space struct layouts align byte-for-byte (bridge in place, full migration pending). +- For Windows, `ebpf/socket.h` now aliases to the canonical shared header (`shared-ebpf/include/gpa_audit_event.h`) so map and redirect-context layouts align with the shared contract. The user-space decoder accepts both the canonical layout and the legacy layout to preserve compatibility with previously shipped eBPF programs. ## 4. Shared Headers @@ -95,7 +95,7 @@ The canonical, platform-neutral structs live in `shared-ebpf/include/gpa_audit_e ## 5. Build System -The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `cargo build`, making it the single source of truth for both `cargo` and the production script: +Linux eBPF objects are compiled by **`proxy_agent/build.rs`** during normal `cargo build`: - Invokes `clang -target bpf -Werror -O2 -g -I shared-ebpf/include -I linux-ebpf -c linux-ebpf/ebpf_cgroup.c -o ebpf_cgroup.o`. `-g` emits the `.BTF`/`.BTF.ext` sections that carry the CO-RE relocation records. - **Arch-aware:** reads `CARGO_CFG_TARGET_ARCH` and selects `-D__TARGET_ARCH_x86` (x86_64) or `-D__TARGET_ARCH_arm64 -I/usr/include/aarch64-linux-gnu` (aarch64), mirroring the two branches that used to live in `build-linux.sh`. @@ -105,6 +105,8 @@ The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `car **`build-linux.sh`** no longer compiles eBPF itself: it adds the shared include path, lets `build.rs` produce `ebpf_cgroup.o`, then verifies the object exists at `$out_dir/ebpf_cgroup.o` before packaging. +Windows eBPF objects are compiled by **`build.cmd`** (`redirect.bpf.o` then `redirect.bpf.sys` via `Convert-BpfToNative.ps1`), matching existing Windows packaging/signing workflow. + - Output name is `ebpf_cgroup.o`, matching `ebpfProgramName` in `proxy_agent/config/GuestProxyAgent.linux.json`. (Future programs — `sk_lookup.o`, `lsm.o` — slot into the `ebpf_programs` list in `build.rs`.) - Requires `clang` 15+. @@ -118,14 +120,14 @@ The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `car - `proxy_agent/src/redirector/linux.rs` loads the object with **aya** (`EbpfLoader` + BTF) and attaches the two programs by name: `connect4` (`CgroupSockAddr`) and `tcp_v4_connect` (`KProbe` on `tcp_connect`). - `proxy_agent/src/redirector/linux/ebpf_obj.rs` keeps the `[u32; N]` ↔ `gpa_*` binary contract for map keys/values. -- Windows side references the shared header for its user-space event layout (bridge); full switch to the shared structs is pending. +- Windows side uses the shared header and dual-decode compatibility in user-space: `lookup_audit` and redirect-context parsing first attempt canonical `sock_addr_audit_entry_t`, validate decoded fields, then fall back to legacy `sock_addr_audit_entry_legacy_t` when needed. - Removed: hardcoded-offset blob read of `sock_common`; replaced with per-field CO-RE reads. ## 8. Tests - `redirector::linux::ebpf_obj` layout unit tests assert the Rust `[u32; N]` mappings match the shared C structs (run on every build). - `redirector::linux::tests::linux_ebpf_test` (feature `test-with-root`, run by `build-linux.sh`) actually **loads and attaches** the CO-RE object on the build host, exercising the kprobe relocation path end-to-end. Bring-up validated it loads cleanly via `bpftool prog loadall` (no unresolved CO-RE relocations, verifier accepts the program). -- Pending: cross-kernel matrix CI (5.4, 5.15, 6.1, 6.8) loading the same object; load-time budget \< 100 ms; Windows layout assertion. +- Pending: cross-kernel matrix CI (5.4, 5.15, 6.1, 6.8) loading the same object; load-time budget \< 100 ms; dedicated Windows compatibility tests that exercise both canonical and legacy audit layouts. ## 9. Risks @@ -133,6 +135,7 @@ The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `car - **Relocating a local copy** — reading into a `preserve_access_index` type overflows the stack copy; always read CO-RE fields into plain scalars. Mitigation: documented in `ebpf_cgroup.c`; load test catches regressions. - **clang/LLVM bugs** in CO-RE relocation. Mitigation: pin clang 15+. - **Stale object pickup** — multiple build-output dirs can leave old objects around. Mitigation: `build.rs` writes the object to a deterministic profile/`deps` path; `build-linux.sh` verifies it. +- **Mixed-version Windows rollout** — new agent with old eBPF program (or inverse) can misinterpret audit value bytes. Mitigation: user-space decode path validates canonical fields and falls back to legacy layout for map lookup and redirect-context reads. ## 10. Milestones @@ -141,7 +144,7 @@ The eBPF object is compiled by **`proxy_agent/build.rs`** during the normal `car | M1 | Linux CO-RE object via aya + preserve_access_index | ✅ `ebpf_cgroup.o` loads/attaches on supported kernels | | M2 | Shared header + Rust layout assertions | ✅ `ebpf_obj` layout tests green on Linux | | M3 | Arch-aware, fail-fast, deterministic build | ✅ `build.rs` (x86_64 + arm64) owns the compile | -| M4 | Windows migration to shared structs | Bridge in place; full switch pending | +| M4 | Windows migration to shared structs | ✅ Shared structs active + legacy decode fallback; `build.cmd` owns `redirect.bpf` build | | M5 | Drop per-kernel builds + cross-kernel CI | CI matrix \< 1/3 of previous count | Detail design for direction 4.2. Parent: [Innovation-Directions.md](Innovation-Directions.md). diff --git a/ebpf/redirect.bpf.c b/ebpf/redirect.bpf.c index 6e9fff22..1acee7c7 100644 --- a/ebpf/redirect.bpf.c +++ b/ebpf/redirect.bpf.c @@ -59,15 +59,20 @@ update_audit_map_entry(bpf_sock_addr_t *ctx) sock_addr_audit_entry_t entry = {0}; entry.process_id = pid; - entry.logon_id = bpf_get_current_logon_id(ctx); + entry.logon_id = (uint32_t)bpf_get_current_logon_id(ctx); if (entry.logon_id == 0) { bpf_printk("Failed to get logon id."); } - entry.is_admin = bpf_is_current_admin(ctx); - if (entry.is_admin < 0) + int32_t is_admin = bpf_is_current_admin(ctx); + if (is_admin < 0) { - bpf_printk("Failed to get admin status %u.", entry.is_admin); + bpf_printk("Failed to get admin status %d.", is_admin); + entry.is_root = 0; + } + else + { + entry.is_root = (is_admin > 0) ? 1 : 0; } entry.destination_ipv4 = ctx->user_ip4; // we only support ipv4 so far. entry.destination_port = ctx->user_port; diff --git a/ebpf/socket.h b/ebpf/socket.h index 9c012a88..693f787c 100644 --- a/ebpf/socket.h +++ b/ebpf/socket.h @@ -9,6 +9,7 @@ #include #include +#include "../shared-ebpf/include/gpa_audit_event.h" // Standard protocol constants #define IPPROTO_TCP 6 @@ -17,46 +18,7 @@ #define AF_INET 2 #define AF_INET6 0x17 -// ============================================================================ -// MIGRATION IN PROGRESS: These typedefs bridge to shared structures -// See ../shared-ebpf/include/gpa_audit_event.h for canonical definitions -// ============================================================================ - -// Destination entry for policy routing -// Note: In CO-RE, this will be unified with Linux version -typedef struct _destination_entry -{ - union { - uint32_t ipv4; - uint32_t ipv6[4]; - } destination_ip; - uint16_t destination_port; - uint32_t protocol; -} destination_entry_t; - -// Audit key for efficient map lookups -// Replaces: struct gpa_audit_key (from shared header) -typedef struct _sock_addr_audit_key -{ - uint32_t protocol; - uint16_t source_port; -} sock_addr_audit_key_t; - -// Audit entry - IMPORTANT: This is being unified with gpa_audit_event -// The canonical structure is now at ../shared-ebpf/include/gpa_audit_event.h -// TODO: Migrate to use the shared struct gpa_audit_event when Windows eBPF -// updates its event ring buffer handling to match Linux/shared layout -typedef struct _sock_addr_audit_entry -{ - uint64_t logon_id; // Will map to uid in shared struct - uint32_t process_id; // Same as pid in shared struct - int32_t is_admin; // Same as is_admin in shared struct - uint32_t destination_ipv4; // Same in shared struct - uint16_t destination_port; // Same in shared struct -} sock_addr_audit_entry_t; - -// Skip process entry -typedef struct _sock_addr_skip_process_entry -{ - uint32_t pid; -} sock_addr_skip_process_entry; \ No newline at end of file +typedef struct gpa_destination_entry destination_entry_t; +typedef struct gpa_audit_key sock_addr_audit_key_t; +typedef struct gpa_audit_event sock_addr_audit_entry_t; +typedef struct gpa_skip_process_entry sock_addr_skip_process_entry; \ No newline at end of file diff --git a/proxy_agent/src/redirector/windows.rs b/proxy_agent/src/redirector/windows.rs index 3f61902a..91613ec7 100644 --- a/proxy_agent/src/redirector/windows.rs +++ b/proxy_agent/src/redirector/windows.rs @@ -5,6 +5,9 @@ mod bpf_api; mod bpf_obj; mod bpf_prog; +use self::bpf_obj::{ + is_valid_new_audit_entry, sock_addr_audit_entry_legacy_t, sock_addr_audit_entry_t, +}; use crate::common::error::{BpfErrorType, Error, WindowsApiErrorType}; use crate::common::{constants, logger, result::Result}; use crate::redirector::AuditEntry; @@ -107,38 +110,84 @@ pub async fn close_bpf_object(redirector_shared_state: RedirectorSharedState) { pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result { // WSAIoctl - SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT - let value = AuditEntry::empty(); - let redirect_context_size = mem::size_of::() as u32; - let mut redirect_context_returned: u32 = 0; - let result = unsafe { + let mut value = sock_addr_audit_entry_t::empty(); + let redirect_context_size_new = mem::size_of::() as u32; + let mut redirect_context_returned_new: u32 = 0; + let result_new = unsafe { WinSock::WSAIoctl( raw_socket_id, WinSock::SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT, ptr::null(), 0, - &value as *const AuditEntry as *mut c_void, - redirect_context_size, - &mut redirect_context_returned, + &mut value as *mut sock_addr_audit_entry_t as *mut c_void, + redirect_context_size_new, + &mut redirect_context_returned_new, ptr::null_mut(), None, ) }; - if result != 0 { - let error = unsafe { WinSock::WSAGetLastError() }; - return Err(Error::WindowsApi(WindowsApiErrorType::WSAIoctl(format!( - "SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT result: {result}, WSAGetLastError: {error}", - )))); + + if result_new == 0 + && redirect_context_returned_new == redirect_context_size_new + && is_valid_new_audit_entry(&value) + { + return Ok(AuditEntry { + logon_id: u64::from(value.logon_id), + process_id: value.process_id, + is_admin: value.is_root as i32, + destination_ipv4: value.destination_ipv4, + destination_port: value.destination_port as u16, + }); + } + + if result_new == 0 && redirect_context_returned_new == redirect_context_size_new { + logger::write( + "redirect context decoded with new layout but failed field validation, falling back to legacy layout" + .to_string(), + ); } - // Need to check the returned size to ensure it matches the expected size, - // since the result is 0 even if there is no redirect context in this socket stream. - if redirect_context_returned != redirect_context_size { - return Err(Error::WindowsApi(WindowsApiErrorType::WSAIoctl(format!( - "SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT returned size: {redirect_context_returned}, expected size: {redirect_context_size}", - )))); + let mut legacy_value = sock_addr_audit_entry_legacy_t::empty(); + let redirect_context_size_legacy = mem::size_of::() as u32; + let mut redirect_context_returned_legacy: u32 = 0; + let result_legacy = unsafe { + WinSock::WSAIoctl( + raw_socket_id, + WinSock::SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT, + ptr::null(), + 0, + &mut legacy_value as *mut sock_addr_audit_entry_legacy_t as *mut c_void, + redirect_context_size_legacy, + &mut redirect_context_returned_legacy, + ptr::null_mut(), + None, + ) + }; + + if result_legacy == 0 && redirect_context_returned_legacy == redirect_context_size_legacy { + return Ok(AuditEntry { + logon_id: legacy_value.logon_id, + process_id: legacy_value.process_id, + is_admin: legacy_value.is_admin, + destination_ipv4: legacy_value.destination_ipv4, + destination_port: legacy_value.destination_port, + }); } - Ok(value) + let new_last_error = if result_new != 0 { + unsafe { WinSock::WSAGetLastError() } + } else { + 0 + }; + let legacy_last_error = if result_legacy != 0 { + unsafe { WinSock::WSAGetLastError() } + } else { + 0 + }; + + Err(Error::WindowsApi(WindowsApiErrorType::WSAIoctl(format!( + "SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT incompatible layout: new(result={result_new}, returned={redirect_context_returned_new}, expected={redirect_context_size_new}, wsa={new_last_error}), legacy(result={result_legacy}, returned={redirect_context_returned_legacy}, expected={redirect_context_size_legacy}, wsa={legacy_last_error})", + )))) } pub async fn update_wire_server_redirect_policy( diff --git a/proxy_agent/src/redirector/windows/bpf_obj.rs b/proxy_agent/src/redirector/windows/bpf_obj.rs index 0337cb3a..c965292e 100644 --- a/proxy_agent/src/redirector/windows/bpf_obj.rs +++ b/proxy_agent/src/redirector/windows/bpf_obj.rs @@ -224,14 +224,63 @@ pub struct bpf_object { #[repr(C)] pub struct _sock_addr_audit_key { pub protocol: u32, - pub source_port: [u16; 2], + pub source_port: u32, } pub type sock_addr_audit_key_t = _sock_addr_audit_key; impl sock_addr_audit_key_t { pub fn from_source_port(port: u16) -> Self { sock_addr_audit_key_t { protocol: IPPROTO_TCP, - source_port: [port.to_be(), 0], + source_port: u32::from(port.to_be()), + } + } +} + +#[repr(C)] +pub struct _sock_addr_audit_entry { + pub logon_id: u32, + pub process_id: u32, + pub is_root: u32, + pub destination_ipv4: u32, + pub destination_port: u32, +} +pub type sock_addr_audit_entry_t = _sock_addr_audit_entry; +impl sock_addr_audit_entry_t { + pub fn empty() -> Self { + sock_addr_audit_entry_t { + logon_id: 0, + process_id: 0, + is_root: 0, + destination_ipv4: 0, + destination_port: 0, + } + } +} + +pub fn is_valid_new_audit_entry(value: &sock_addr_audit_entry_t) -> bool { + // The canonical/shared layout encodes destination_port as u32 with a u16 + // network-byte-order value in the low 16 bits, and is_root as a boolean. + value.is_root <= 1 && (value.destination_port & 0xFFFF_0000) == 0 +} + +// Legacy layout used by older Windows eBPF programs before shared struct migration. +#[repr(C)] +pub struct _sock_addr_audit_entry_legacy { + pub logon_id: u64, + pub process_id: u32, + pub is_admin: i32, + pub destination_ipv4: u32, + pub destination_port: u16, +} +pub type sock_addr_audit_entry_legacy_t = _sock_addr_audit_entry_legacy; +impl sock_addr_audit_entry_legacy_t { + pub fn empty() -> Self { + sock_addr_audit_entry_legacy_t { + logon_id: 0, + process_id: 0, + is_admin: 0, + destination_ipv4: 0, + destination_port: 0, } } } @@ -264,14 +313,14 @@ pub type ip_address_t = _ip_address; #[repr(C)] pub struct _destination_entry { pub destination_ip: ip_address_t, - pub destination_port: [u16; 2], // first element is the port number, second element is empty + pub destination_port: u32, pub protocol: u32, } impl _destination_entry { pub fn empty() -> Self { _destination_entry { destination_ip: ip_address_t::empty(), - destination_port: [0, 0], + destination_port: 0, protocol: IPPROTO_TCP, } } @@ -279,7 +328,7 @@ impl _destination_entry { pub fn from_ipv4(ipv4: u32, port: u16) -> Self { let mut entry = Self::empty(); entry.destination_ip = ip_address_t::from_ipv4(ipv4); - entry.destination_port[0] = port.to_be(); + entry.destination_port = u32::from(port.to_be()); entry } } diff --git a/proxy_agent/src/redirector/windows/bpf_prog.rs b/proxy_agent/src/redirector/windows/bpf_prog.rs index 6244363d..3318bf3c 100644 --- a/proxy_agent/src/redirector/windows/bpf_prog.rs +++ b/proxy_agent/src/redirector/windows/bpf_prog.rs @@ -268,12 +268,43 @@ impl BpfObject { // query by source port. let key = sock_addr_audit_key_t::from_source_port(source_port); - let value = AuditEntry::empty(); + let mut value = sock_addr_audit_entry_t::empty(); + let result_new = bpf_map_lookup_elem( + map_fd, + &key as *const sock_addr_audit_key_t as *const c_void, + &mut value as *mut sock_addr_audit_entry_t as *mut c_void, + ) + .map_err(|e| { + Error::Bpf(BpfErrorType::MapLookupElem( + source_port.to_string(), + format!("Error: {e}"), + )) + })?; + + if result_new == 0 && is_valid_new_audit_entry(&value) { + return Ok(AuditEntry { + logon_id: u64::from(value.logon_id), + process_id: value.process_id, + is_admin: value.is_root as i32, + destination_ipv4: value.destination_ipv4, + destination_port: value.destination_port as u16, + }); + } - let result = bpf_map_lookup_elem( + if result_new == 0 { + logger::write(format!( + "audit_map entry for source_port={} decoded with new layout but failed validation, falling back to legacy layout", + source_port + )); + } + + // Backward compatibility: older Windows eBPF programs use a larger + // audit entry layout (u64 logon_id, i32 is_admin, u16 destination_port). + let mut legacy_value = sock_addr_audit_entry_legacy_t::empty(); + let result_legacy = bpf_map_lookup_elem( map_fd, &key as *const sock_addr_audit_key_t as *const c_void, - &value as *const AuditEntry as *mut c_void, + &mut legacy_value as *mut sock_addr_audit_entry_legacy_t as *mut c_void, ) .map_err(|e| { Error::Bpf(BpfErrorType::MapLookupElem( @@ -282,14 +313,20 @@ impl BpfObject { )) })?; - if result != 0 { + if result_legacy != 0 { return Err(Error::Bpf(BpfErrorType::MapLookupElem( source_port.to_string(), - format!("Result: {result}"), + format!("Result new={result_new}, legacy={result_legacy}"), ))); } - Ok(value) + Ok(AuditEntry { + logon_id: legacy_value.logon_id, + process_id: legacy_value.process_id, + is_admin: legacy_value.is_admin, + destination_ipv4: legacy_value.destination_ipv4, + destination_port: legacy_value.destination_port, + }) } /** From 02e2199546f7c66586d082f9d150b1539ccca64a Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Thu, 18 Jun 2026 09:07:01 -0700 Subject: [PATCH 05/10] use ConnectionLogger at lookup_audit --- proxy_agent/src/proxy/proxy_connection.rs | 4 ++-- proxy_agent/src/redirector.rs | 10 +++++++--- proxy_agent/src/redirector/linux.rs | 6 +++++- proxy_agent/src/redirector/windows.rs | 9 +++++++-- proxy_agent/src/redirector/windows/bpf_prog.rs | 14 ++++++++++---- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/proxy_agent/src/proxy/proxy_connection.rs b/proxy_agent/src/proxy/proxy_connection.rs index a810c77a..7fd49d71 100644 --- a/proxy_agent/src/proxy/proxy_connection.rs +++ b/proxy_agent/src/proxy/proxy_connection.rs @@ -152,7 +152,7 @@ impl TcpConnectionContext { #[cfg(windows)] raw_socket_id: usize, ) -> Result { let client_source_port = client_addr.port(); - match redirector::lookup_audit(client_source_port, redirector_shared_state).await { + match redirector::lookup_audit(client_source_port, redirector_shared_state, logger).await { Ok(data) => { logger.write( LoggerLevel::Trace, @@ -195,7 +195,7 @@ impl TcpConnectionContext { "Try to get audit entry from socket stream".to_string(), ); - match redirector::get_audit_from_stream_socket(raw_socket_id) { + match redirector::get_audit_from_stream_socket(raw_socket_id, logger) { Ok(data) => { logger.write( LoggerLevel::Info, diff --git a/proxy_agent/src/redirector.rs b/proxy_agent/src/redirector.rs index 2dc87a46..1bca630e 100644 --- a/proxy_agent/src/redirector.rs +++ b/proxy_agent/src/redirector.rs @@ -273,8 +273,11 @@ impl Redirector { } #[cfg(windows)] -pub fn get_audit_from_stream_socket(raw_socket_id: usize) -> Result { - windows::get_audit_from_redirect_context(raw_socket_id) +pub fn get_audit_from_stream_socket( + raw_socket_id: usize, + logger: &mut crate::proxy::proxy_connection::ConnectionLogger, +) -> Result { + windows::get_audit_from_redirect_context(raw_socket_id, logger) } pub fn ip_to_string(ip: u32) -> String { @@ -354,9 +357,10 @@ pub fn get_ebpf_file_path() -> PathBuf { pub async fn lookup_audit( source_port: u16, redirector_shared_state: &RedirectorSharedState, + logger: &mut crate::proxy::proxy_connection::ConnectionLogger, ) -> Result { if let Ok(Some(bpf_object)) = redirector_shared_state.get_bpf_object().await { - bpf_object.lock().unwrap().lookup_audit(source_port) + bpf_object.lock().unwrap().lookup_audit(source_port, logger) } else { Err(Error::Bpf(BpfErrorType::NullBpfObject)) } diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index cf5e1776..65852d11 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -238,7 +238,11 @@ impl BpfObject { Ok(()) } - pub fn lookup_audit(&self, source_port: u16) -> Result { + pub fn lookup_audit( + &self, + source_port: u16, + _logger: &mut crate::proxy::proxy_connection::ConnectionLogger, + ) -> Result { let audit_map_name = "audit_map"; match self.0.map(audit_map_name) { Some(map) => match HashMap::try_from(map) { diff --git a/proxy_agent/src/redirector/windows.rs b/proxy_agent/src/redirector/windows.rs index 91613ec7..3b64a088 100644 --- a/proxy_agent/src/redirector/windows.rs +++ b/proxy_agent/src/redirector/windows.rs @@ -108,7 +108,10 @@ pub async fn close_bpf_object(redirector_shared_state: RedirectorSharedState) { } } -pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result { +pub fn get_audit_from_redirect_context( + raw_socket_id: usize, + logger: &mut crate::proxy::proxy_connection::ConnectionLogger, +) -> Result { // WSAIoctl - SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT let mut value = sock_addr_audit_entry_t::empty(); let redirect_context_size_new = mem::size_of::() as u32; @@ -141,7 +144,9 @@ pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result Result { + pub fn lookup_audit( + &self, + source_port: u16, + logger: &mut crate::proxy::proxy_connection::ConnectionLogger, + ) -> Result { let map_name = "audit_map"; let map_fd = self.get_bpf_map_fd(map_name)?; @@ -292,7 +296,9 @@ impl BpfObject { } if result_new == 0 { - logger::write(format!( + // Log a trace message if the new layout is decoded but fails validation. + // It is normal for the new layout with older eBPF programs to be used in a different context. + logger.write(proxy_agent_shared::logger::LoggerLevel::Trace, format!( "audit_map entry for source_port={} decoded with new layout but failed validation, falling back to legacy layout", source_port )); From db175c5d7af67f3ff1e6f720d806a670cffab434 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Thu, 18 Jun 2026 16:10:46 +0000 Subject: [PATCH 06/10] fix linux build --- proxy_agent/src/redirector/linux.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index 65852d11..dea68858 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -566,7 +566,8 @@ mod tests { ); let source_port = 1; - let audit = bpf.lookup_audit(source_port); + let logger = &mut crate::proxy::proxy_connection::ConnectionLogger::new(1, 1); + let audit = bpf.lookup_audit(source_port, logger); assert!( audit.is_err(), "lookup_audit should return error for invalid source port" @@ -591,7 +592,7 @@ mod tests { .insert(key.to_array(), value.to_array(), 0) .unwrap(); } - let audit = bpf.lookup_audit(source_port); + let audit = bpf.lookup_audit(source_port, logger); match audit { Ok(entry) => { assert_eq!( From 5243df7f9a968fb451ffbef67809c69aed4f6121 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Thu, 18 Jun 2026 21:42:47 +0000 Subject: [PATCH 07/10] fix bloat for linux build --- .github/workflows/bloat.yml | 12 ++++++------ proxy_agent/build.rs | 8 +++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/bloat.yml b/.github/workflows/bloat.yml index 78b47989..deafb36f 100644 --- a/.github/workflows/bloat.yml +++ b/.github/workflows/bloat.yml @@ -44,13 +44,13 @@ jobs: # Vendored OpenSSL (approved crypto) is structurally ~20% of text # for the agent on musl; HMAC-SHA256 in proxy_agent_shared pulls it. crate_share_overrides: "openssl_sys=0.25" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm - target: x86_64-unknown-linux-musl runs_on: ubuntu-latest crate: ProxyAgentExt max_binary_bytes: "9000000" # ~9 MB crate_share_overrides: "clap_builder=0.15 regex_automata=0.15" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm - target: x86_64-unknown-linux-musl runs_on: ubuntu-latest crate: proxy_agent_setup @@ -58,7 +58,7 @@ jobs: # proxy_agent_setup is tiny (~1 MiB text after the openssl gate), # so a normal-sized clap derive parser is ~30% by share. crate_share_overrides: "clap_builder=0.35" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm # -------- Linux aarch64 musl (native arm64 runner) -------- - target: aarch64-unknown-linux-musl @@ -66,19 +66,19 @@ jobs: crate: azure-proxy-agent max_binary_bytes: "20000000" crate_share_overrides: "openssl_sys=0.15" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm - target: aarch64-unknown-linux-musl runs_on: ubuntu-24.04-arm crate: ProxyAgentExt max_binary_bytes: "16000000" crate_share_overrides: "clap_builder=0.15 regex_automata=0.15" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm - target: aarch64-unknown-linux-musl runs_on: ubuntu-24.04-arm crate: proxy_agent_setup max_binary_bytes: "11000000" crate_share_overrides: "clap_builder=0.35" - apt_packages: musl-tools + apt_packages: musl-tools libbpf-dev libbpfcc-dev clang llvm # -------- Windows x86_64 MSVC -------- - target: x86_64-pc-windows-msvc diff --git a/proxy_agent/build.rs b/proxy_agent/build.rs index 0898fdf4..2a514de9 100644 --- a/proxy_agent/build.rs +++ b/proxy_agent/build.rs @@ -48,7 +48,13 @@ fn compile_ebpf_with_core() { "-D__TARGET_ARCH_arm64", Some("-I/usr/include/aarch64-linux-gnu".to_string()), ), - _ => ("-D__TARGET_ARCH_x86", None), + // The x86_64 multiarch include is required so clang (-target bpf) can + // resolve `asm/types.h` (pulled in by ) on hosts that do + // not install the gcc-multilib `/usr/include/asm` compatibility symlink. + _ => ( + "-D__TARGET_ARCH_x86", + Some("-I/usr/include/x86_64-linux-gnu".to_string()), + ), }; // Build flags for CO-RE compilation. These mirror build-linux.sh so both From 87c1542fc4faf1c268b231c392c3a91977b58372 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 10 Jun 2026 13:53:30 -0700 Subject: [PATCH 08/10] Add innovation plan files (#355) * Add innovation plan files * Update 7.3 md files * fix spelling * fix * update spelling * Update spelling * Update check-spelling metadata --------- Co-authored-by: Zhidong Peng --- .github/workflows/ci.yml | 4 ++++ .github/workflows/spelling.yml | 2 +- cspell.json | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bd5544a..664169ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install eBPF build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libbpf-dev libbpfcc-dev clang llvm - uses: Swatinem/rust-cache@v2 - uses: dtolnay/rust-toolchain@stable with: diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 0d47f186..c85e2e0c 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Check Spelling - uses: streetsidesoftware/cspell-action@v6 + uses: streetsidesoftware/cspell-action@v8 with: config: cspell.json # On pull requests, only check files changed in the PR. diff --git a/cspell.json b/cspell.json index fd1f639a..73f488e5 100644 --- a/cspell.json +++ b/cspell.json @@ -299,6 +299,7 @@ "llvmorg", "logdir", "logon", + "lossily", "Lrs", "lsa", "lsm", @@ -420,6 +421,7 @@ "Razr", "rcv", "RDFE", + "recompiles", "RECVORIGDSTADDR", "redhat", "Redist", From 919a798d3ff79b1c2a66e3f05e7ac443ef366ffb Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Thu, 18 Jun 2026 22:04:46 +0000 Subject: [PATCH 09/10] fix spell --- cspell.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cspell.json b/cspell.json index 73f488e5..aae8bf9e 100644 --- a/cspell.json +++ b/cspell.json @@ -297,6 +297,7 @@ "linkid", "llc", "llvmorg", + "loadall", "logdir", "logon", "lossily", @@ -327,6 +328,7 @@ "msc", "msrc", "MSRs", + "multiarch", "multilib", "nbf", "ncurl", @@ -359,6 +361,7 @@ "ntimeout", "ntohs", "NTSTATUS", + "objdump", "OICI", "OIDC", "onebranch", From ab22046eb270b67823623c8128655a17bce99ead Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Tue, 7 Jul 2026 12:55:51 -0700 Subject: [PATCH 10/10] resolve AI Comments --- proxy_agent/src/proxy/proxy_connection.rs | 4 +- proxy_agent/src/redirector.rs | 10 +- proxy_agent/src/redirector/linux.rs | 11 +- proxy_agent/src/redirector/windows.rs | 93 ++--------- proxy_agent/src/redirector/windows/bpf_obj.rs | 157 +++++++++++++++++- .../src/redirector/windows/bpf_prog.rs | 71 ++------ shared-ebpf/include/gpa_audit_event.h | 43 +++-- 7 files changed, 218 insertions(+), 171 deletions(-) diff --git a/proxy_agent/src/proxy/proxy_connection.rs b/proxy_agent/src/proxy/proxy_connection.rs index c842d6d7..20fe40db 100644 --- a/proxy_agent/src/proxy/proxy_connection.rs +++ b/proxy_agent/src/proxy/proxy_connection.rs @@ -152,7 +152,7 @@ impl TcpConnectionContext { #[cfg(windows)] raw_socket_id: usize, ) -> Result { let client_source_port = client_addr.port(); - match redirector::lookup_audit(client_source_port, redirector_shared_state, logger).await { + match redirector::lookup_audit(client_source_port, redirector_shared_state).await { Ok(data) => { logger.write( LoggerLevel::Trace, @@ -195,7 +195,7 @@ impl TcpConnectionContext { "Try to get audit entry from socket stream".to_string(), ); - match redirector::get_audit_from_stream_socket(raw_socket_id, logger) { + match redirector::get_audit_from_stream_socket(raw_socket_id) { Ok(data) => { logger.write( LoggerLevel::Info, diff --git a/proxy_agent/src/redirector.rs b/proxy_agent/src/redirector.rs index 1bca630e..2dc87a46 100644 --- a/proxy_agent/src/redirector.rs +++ b/proxy_agent/src/redirector.rs @@ -273,11 +273,8 @@ impl Redirector { } #[cfg(windows)] -pub fn get_audit_from_stream_socket( - raw_socket_id: usize, - logger: &mut crate::proxy::proxy_connection::ConnectionLogger, -) -> Result { - windows::get_audit_from_redirect_context(raw_socket_id, logger) +pub fn get_audit_from_stream_socket(raw_socket_id: usize) -> Result { + windows::get_audit_from_redirect_context(raw_socket_id) } pub fn ip_to_string(ip: u32) -> String { @@ -357,10 +354,9 @@ pub fn get_ebpf_file_path() -> PathBuf { pub async fn lookup_audit( source_port: u16, redirector_shared_state: &RedirectorSharedState, - logger: &mut crate::proxy::proxy_connection::ConnectionLogger, ) -> Result { if let Ok(Some(bpf_object)) = redirector_shared_state.get_bpf_object().await { - bpf_object.lock().unwrap().lookup_audit(source_port, logger) + bpf_object.lock().unwrap().lookup_audit(source_port) } else { Err(Error::Bpf(BpfErrorType::NullBpfObject)) } diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index dea68858..cf5e1776 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -238,11 +238,7 @@ impl BpfObject { Ok(()) } - pub fn lookup_audit( - &self, - source_port: u16, - _logger: &mut crate::proxy::proxy_connection::ConnectionLogger, - ) -> Result { + pub fn lookup_audit(&self, source_port: u16) -> Result { let audit_map_name = "audit_map"; match self.0.map(audit_map_name) { Some(map) => match HashMap::try_from(map) { @@ -566,8 +562,7 @@ mod tests { ); let source_port = 1; - let logger = &mut crate::proxy::proxy_connection::ConnectionLogger::new(1, 1); - let audit = bpf.lookup_audit(source_port, logger); + let audit = bpf.lookup_audit(source_port); assert!( audit.is_err(), "lookup_audit should return error for invalid source port" @@ -592,7 +587,7 @@ mod tests { .insert(key.to_array(), value.to_array(), 0) .unwrap(); } - let audit = bpf.lookup_audit(source_port, logger); + let audit = bpf.lookup_audit(source_port); match audit { Ok(entry) => { assert_eq!( diff --git a/proxy_agent/src/redirector/windows.rs b/proxy_agent/src/redirector/windows.rs index 3b64a088..c9238124 100644 --- a/proxy_agent/src/redirector/windows.rs +++ b/proxy_agent/src/redirector/windows.rs @@ -5,15 +5,11 @@ mod bpf_api; mod bpf_obj; mod bpf_prog; -use self::bpf_obj::{ - is_valid_new_audit_entry, sock_addr_audit_entry_legacy_t, sock_addr_audit_entry_t, -}; use crate::common::error::{BpfErrorType, Error, WindowsApiErrorType}; use crate::common::{constants, logger, result::Result}; +use crate::redirector::windows::bpf_obj::AuditValueEntry; use crate::redirector::AuditEntry; use crate::shared_state::redirector_wrapper::RedirectorSharedState; -use core::ffi::c_void; -use std::mem; use std::ptr; use windows_sys::Win32::Networking::WinSock; @@ -108,91 +104,32 @@ pub async fn close_bpf_object(redirector_shared_state: RedirectorSharedState) { } } -pub fn get_audit_from_redirect_context( - raw_socket_id: usize, - logger: &mut crate::proxy::proxy_connection::ConnectionLogger, -) -> Result { - // WSAIoctl - SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT - let mut value = sock_addr_audit_entry_t::empty(); - let redirect_context_size_new = mem::size_of::() as u32; - let mut redirect_context_returned_new: u32 = 0; - let result_new = unsafe { +pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result { + // We do not know the value size here, pass 0 to use its max_value_size + let mut audit_value_entry = AuditValueEntry::empty(0); + let mut redirect_context_returned: u32 = 0; + let result = unsafe { WinSock::WSAIoctl( raw_socket_id, WinSock::SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT, ptr::null(), 0, - &mut value as *mut sock_addr_audit_entry_t as *mut c_void, - redirect_context_size_new, - &mut redirect_context_returned_new, + audit_value_entry.value_pointer_mut(), + audit_value_entry.value_size(), // large enough for either layout + &mut redirect_context_returned, ptr::null_mut(), None, ) }; - if result_new == 0 - && redirect_context_returned_new == redirect_context_size_new - && is_valid_new_audit_entry(&value) - { - return Ok(AuditEntry { - logon_id: u64::from(value.logon_id), - process_id: value.process_id, - is_admin: value.is_root as i32, - destination_ipv4: value.destination_ipv4, - destination_port: value.destination_port as u16, - }); + if result != 0 { + let wsa_error = unsafe { WinSock::WSAGetLastError() }; + return Err(Error::WindowsApi(WindowsApiErrorType::WSAIoctl(format!( + "SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT failed: result={result}, WSAGetLastError={wsa_error}", + )))); } - if result_new == 0 && redirect_context_returned_new == redirect_context_size_new { - // Log a trace message if the new layout is decoded but fails validation. - // It is normal for the new layout with older eBPF programs to be used in a different context. - logger.write(proxy_agent_shared::logger::LoggerLevel::Trace, - "redirect context decoded with new layout but failed field validation, falling back to legacy layout" - .to_string(), - ); - } - - let mut legacy_value = sock_addr_audit_entry_legacy_t::empty(); - let redirect_context_size_legacy = mem::size_of::() as u32; - let mut redirect_context_returned_legacy: u32 = 0; - let result_legacy = unsafe { - WinSock::WSAIoctl( - raw_socket_id, - WinSock::SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT, - ptr::null(), - 0, - &mut legacy_value as *mut sock_addr_audit_entry_legacy_t as *mut c_void, - redirect_context_size_legacy, - &mut redirect_context_returned_legacy, - ptr::null_mut(), - None, - ) - }; - - if result_legacy == 0 && redirect_context_returned_legacy == redirect_context_size_legacy { - return Ok(AuditEntry { - logon_id: legacy_value.logon_id, - process_id: legacy_value.process_id, - is_admin: legacy_value.is_admin, - destination_ipv4: legacy_value.destination_ipv4, - destination_port: legacy_value.destination_port, - }); - } - - let new_last_error = if result_new != 0 { - unsafe { WinSock::WSAGetLastError() } - } else { - 0 - }; - let legacy_last_error = if result_legacy != 0 { - unsafe { WinSock::WSAGetLastError() } - } else { - 0 - }; - - Err(Error::WindowsApi(WindowsApiErrorType::WSAIoctl(format!( - "SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT incompatible layout: new(result={result_new}, returned={redirect_context_returned_new}, expected={redirect_context_size_new}, wsa={new_last_error}), legacy(result={result_legacy}, returned={redirect_context_returned_legacy}, expected={redirect_context_size_legacy}, wsa={legacy_last_error})", - )))) + audit_value_entry.to_audit_entry() } pub async fn update_wire_server_redirect_policy( diff --git a/proxy_agent/src/redirector/windows/bpf_obj.rs b/proxy_agent/src/redirector/windows/bpf_obj.rs index c965292e..8244d6e2 100644 --- a/proxy_agent/src/redirector/windows/bpf_obj.rs +++ b/proxy_agent/src/redirector/windows/bpf_obj.rs @@ -2,7 +2,12 @@ // SPDX-License-Identifier: MIT #![allow(non_camel_case_types)] +use crate::common::{ + error::{BpfErrorType, Error}, + result::Result, +}; use std::ffi::c_char; +use std::ffi::c_void; pub type ebpf_id_t = u32; pub type fd_t = i32; @@ -255,12 +260,20 @@ impl sock_addr_audit_entry_t { destination_port: 0, } } + + pub fn to_audit_entry(&self) -> crate::redirector::AuditEntry { + crate::redirector::AuditEntry { + logon_id: self.logon_id as u64, + process_id: self.process_id, + is_admin: self.is_root as i32, + destination_ipv4: self.destination_ipv4, + destination_port: self.destination_port as u16, + } + } } -pub fn is_valid_new_audit_entry(value: &sock_addr_audit_entry_t) -> bool { - // The canonical/shared layout encodes destination_port as u32 with a u16 - // network-byte-order value in the low 16 bits, and is_root as a boolean. - value.is_root <= 1 && (value.destination_port & 0xFFFF_0000) == 0 +pub fn bpf_map_value_size(map: *const bpf_map) -> u32 { + unsafe { (*map).map_definition.value_size } } // Legacy layout used by older Windows eBPF programs before shared struct migration. @@ -283,6 +296,142 @@ impl sock_addr_audit_entry_legacy_t { destination_port: 0, } } + + pub fn to_audit_entry(&self) -> crate::redirector::AuditEntry { + crate::redirector::AuditEntry { + logon_id: self.logon_id, + process_id: self.process_id, + is_admin: self.is_admin, + destination_ipv4: self.destination_ipv4, + destination_port: self.destination_port, + } + } +} + +/// Represents an unknown audit value entry. +pub struct GenericAuditValueEntry { + /// Owned bytes for unknown layout values. + /// + /// We keep this as raw bytes and decode based on `read_size` to avoid + /// assuming a concrete Rust type up front. + pub buffer: Vec, + /// Allocated size of the audit value data. + pub data_size: u32, + /// Number of bytes read from the unknown audit value data. + pub read_size: u32, +} + +pub(crate) enum AuditValueEntry { + New(sock_addr_audit_entry_t), + Legacy(sock_addr_audit_entry_legacy_t), + // if Unknown type, use the generic audit value entry + Unknown(GenericAuditValueEntry), +} + +impl AuditValueEntry { + const VALUE_SIZE_NEW: u32 = std::mem::size_of::() as u32; + const VALUE_SIZE_LEGACY: u32 = std::mem::size_of::() as u32; + + /// return the maximum value size among all audit entry types. + fn max_value_size() -> u32 { + Self::VALUE_SIZE_NEW.max(Self::VALUE_SIZE_LEGACY) + } + + pub fn empty(value_size: u32) -> Self { + match value_size { + Self::VALUE_SIZE_NEW => AuditValueEntry::New(sock_addr_audit_entry_t::empty()), + Self::VALUE_SIZE_LEGACY => { + AuditValueEntry::Legacy(sock_addr_audit_entry_legacy_t::empty()) + } + _ => { + let max_value_size = Self::max_value_size(); + AuditValueEntry::Unknown(GenericAuditValueEntry { + // Allocate empty buffer with the maximum value size we currently supported. + buffer: vec![0u8; max_value_size as usize], + data_size: max_value_size, + // For map lookups, value_size is fixed per map and equals the + // number of bytes written by bpf_map_lookup_elem. + read_size: value_size, + }) + } + } + } + + pub fn value_size(&self) -> u32 { + match self { + AuditValueEntry::New(_) => Self::VALUE_SIZE_NEW, + AuditValueEntry::Legacy(_) => Self::VALUE_SIZE_LEGACY, + AuditValueEntry::Unknown(entry) => entry.data_size, + } + } + + pub fn value_pointer_mut(&mut self) -> *mut c_void { + match self { + AuditValueEntry::New(entry) => entry as *mut sock_addr_audit_entry_t as *mut c_void, + AuditValueEntry::Legacy(entry) => { + entry as *mut sock_addr_audit_entry_legacy_t as *mut c_void + } + AuditValueEntry::Unknown(entry) => entry.buffer.as_mut_ptr() as *mut c_void, + } + } + + pub fn to_audit_entry(&self) -> Result { + match self { + AuditValueEntry::New(entry) => Ok(entry.to_audit_entry()), + AuditValueEntry::Legacy(entry) => Ok(entry.to_audit_entry()), + AuditValueEntry::Unknown(entry) => { + // Cast the raw bytes to the concrete audit value type based on + // the number of bytes read. + match entry.read_size { + Self::VALUE_SIZE_NEW => { + if entry.buffer.len() < Self::VALUE_SIZE_NEW as usize { + return Err(Error::Bpf(BpfErrorType::MapLookupElem( + "into_audit_entry".to_string(), + format!( + "Insufficient buffer length {} for canonical audit entry size {}", + entry.buffer.len(), + Self::VALUE_SIZE_NEW + ), + ))); + } + + let value = unsafe { + std::ptr::read_unaligned( + entry.buffer.as_ptr() as *const sock_addr_audit_entry_t + ) + }; + Ok(value.to_audit_entry()) + } + Self::VALUE_SIZE_LEGACY => { + if entry.buffer.len() < Self::VALUE_SIZE_LEGACY as usize { + return Err(Error::Bpf(BpfErrorType::MapLookupElem( + "into_audit_entry".to_string(), + format!( + "Insufficient buffer length {} for legacy audit entry size {}", + entry.buffer.len(), + Self::VALUE_SIZE_LEGACY + ), + ))); + } + + let value = unsafe { + std::ptr::read_unaligned( + entry.buffer.as_ptr() as *const sock_addr_audit_entry_legacy_t + ) + }; + Ok(value.to_audit_entry()) + } + _ => Err(Error::Bpf(BpfErrorType::MapLookupElem( + "into_audit_entry".to_string(), + format!( + "Invalid audit value size read: {}, allocated: {}", + entry.read_size, entry.data_size + ), + ))), + } + } + } + } } #[repr(C)] diff --git a/proxy_agent/src/redirector/windows/bpf_prog.rs b/proxy_agent/src/redirector/windows/bpf_prog.rs index 57401e5a..3a0fb5e9 100644 --- a/proxy_agent/src/redirector/windows/bpf_prog.rs +++ b/proxy_agent/src/redirector/windows/bpf_prog.rs @@ -256,27 +256,21 @@ impl BpfObject { source_port - source local port. - logger - connection logger. - Return Value: audit entry from audit_map on success. On failure appropriate RESULT is returned. */ - pub fn lookup_audit( - &self, - source_port: u16, - logger: &mut crate::proxy::proxy_connection::ConnectionLogger, - ) -> Result { + pub fn lookup_audit(&self, source_port: u16) -> Result { let map_name = "audit_map"; - let map_fd = self.get_bpf_map_fd(map_name)?; + let (map_fd, value_size) = self.get_bpf_map_fd_and_value_size(map_name)?; // query by source port. let key = sock_addr_audit_key_t::from_source_port(source_port); - let mut value = sock_addr_audit_entry_t::empty(); - let result_new = bpf_map_lookup_elem( + let mut audit_value_entry = AuditValueEntry::empty(value_size); + let result = bpf_map_lookup_elem( map_fd, &key as *const sock_addr_audit_key_t as *const c_void, - &mut value as *mut sock_addr_audit_entry_t as *mut c_void, + audit_value_entry.value_pointer_mut(), ) .map_err(|e| { Error::Bpf(BpfErrorType::MapLookupElem( @@ -285,54 +279,14 @@ impl BpfObject { )) })?; - if result_new == 0 && is_valid_new_audit_entry(&value) { - return Ok(AuditEntry { - logon_id: u64::from(value.logon_id), - process_id: value.process_id, - is_admin: value.is_root as i32, - destination_ipv4: value.destination_ipv4, - destination_port: value.destination_port as u16, - }); - } - - if result_new == 0 { - // Log a trace message if the new layout is decoded but fails validation. - // It is normal for the new layout with older eBPF programs to be used in a different context. - logger.write(proxy_agent_shared::logger::LoggerLevel::Trace, format!( - "audit_map entry for source_port={} decoded with new layout but failed validation, falling back to legacy layout", - source_port - )); - } - - // Backward compatibility: older Windows eBPF programs use a larger - // audit entry layout (u64 logon_id, i32 is_admin, u16 destination_port). - let mut legacy_value = sock_addr_audit_entry_legacy_t::empty(); - let result_legacy = bpf_map_lookup_elem( - map_fd, - &key as *const sock_addr_audit_key_t as *const c_void, - &mut legacy_value as *mut sock_addr_audit_entry_legacy_t as *mut c_void, - ) - .map_err(|e| { - Error::Bpf(BpfErrorType::MapLookupElem( - source_port.to_string(), - format!("Error: {e}"), - )) - })?; - - if result_legacy != 0 { + if result != 0 { return Err(Error::Bpf(BpfErrorType::MapLookupElem( source_port.to_string(), - format!("Result new={result_new}, legacy={result_legacy}"), + format!("Result: {result}"), ))); } - Ok(AuditEntry { - logon_id: legacy_value.logon_id, - process_id: legacy_value.process_id, - is_admin: legacy_value.is_admin, - destination_ipv4: legacy_value.destination_ipv4, - destination_port: legacy_value.destination_port, - }) + audit_value_entry.to_audit_entry() } /** @@ -439,6 +393,11 @@ impl BpfObject { } fn get_bpf_map_fd(&self, map_name: &str) -> Result { + let (map_fd, _) = self.get_bpf_map_fd_and_value_size(map_name)?; + Ok(map_fd) + } + + fn get_bpf_map_fd_and_value_size(&self, map_name: &str) -> Result<(i32, u32)> { if self.is_null() { return Err(Error::Bpf(BpfErrorType::NullBpfObject)); } @@ -453,6 +412,8 @@ impl BpfObject { ))); } - bpf_map__fd(bpf_map).map_err(|e| Error::Bpf(BpfErrorType::MapFileDescriptor(e.to_string()))) + let map_fd = bpf_map__fd(bpf_map) + .map_err(|e| Error::Bpf(BpfErrorType::MapFileDescriptor(e.to_string())))?; + Ok((map_fd, bpf_map_value_size(bpf_map))) } } diff --git a/shared-ebpf/include/gpa_audit_event.h b/shared-ebpf/include/gpa_audit_event.h index bbeb082f..667cd36f 100644 --- a/shared-ebpf/include/gpa_audit_event.h +++ b/shared-ebpf/include/gpa_audit_event.h @@ -14,8 +14,10 @@ // IP address - union allows IPv4 (first element) or IPv6 (all 4 elements) // Size: 16 bytes (4 x u32) - matches Rust _ip_address { ip: [u32; 4] } -struct gpa_ip_address { - union { +struct gpa_ip_address +{ + union + { __u32 ipv4; __u32 ipv6[4]; }; @@ -24,17 +26,19 @@ struct gpa_ip_address { // Policy destination entry - defines where traffic should be redirected // Size: 24 bytes - matches Rust destination_entry -> [u32; 6] // ip_address(16) + destination_port(4) + protocol(4) -struct gpa_destination_entry { +struct gpa_destination_entry +{ struct gpa_ip_address destination_ip; - __u32 destination_port; // Port stored as u32 (network byte order in lower 16 bits) - __u32 protocol; // IPPROTO_TCP (6) or IPPROTO_UDP (17) + __u32 destination_port; // Port stored as u32 (network byte order in lower 16 bits) + __u32 protocol; // IPPROTO_TCP (6) or IPPROTO_UDP (17) }; // Minimal audit key for map lookups (protocol + source port) // Size: 8 bytes - matches Rust sock_addr_audit_key -> [u32; 2] -struct gpa_audit_key { - __u32 protocol; // IPPROTO_TCP or IPPROTO_UDP - __u32 source_port; // Local source port (stored as u32) +struct gpa_audit_key +{ + __u32 protocol; // IPPROTO_TCP or IPPROTO_UDP + __u32 source_port; // Local source port (stored as u32) }; // Canonical audit event entry - the record stored in the audit map @@ -43,24 +47,27 @@ struct gpa_audit_key { // NOTE: Field names use Linux semantics (logon_id=uid, is_root). The Windows // side maps these to its own naming (logon_id, is_admin) at user-space. // This is the CANONICAL shared struct - both platforms agree on this layout. -struct gpa_audit_event { - __u32 logon_id; // Linux: uid; Windows: logon_id (lower 32 bits) - __u32 process_id; // Process ID - __u32 is_root; // 1 if root/admin, 0 otherwise - __u32 destination_ipv4; // Destination IPv4 address - __u32 destination_port; // Destination port (stored as u32) +struct gpa_audit_event +{ + __u32 logon_id; // Linux: uid; Windows: logon_id (lower 32 bits) + __u32 process_id; // Process ID + __u32 is_root; // 1 if root/admin, 0 otherwise + __u32 destination_ipv4; // Destination IPv4 address + __u32 destination_port; // Destination port (stored as u32) }; // Skip process entry - processes in this map bypass audit/redirect // Size: 4 bytes - matches Rust sock_addr_skip_process_entry -> [u32; 1] -struct gpa_skip_process_entry { +struct gpa_skip_process_entry +{ __u32 pid; }; // Local address entry - tracks current connection state in the local_map // Size: 24 bytes (6 x u32) -struct gpa_sock_addr_local_entry { - __u32 logon_id; // uid +struct gpa_sock_addr_local_entry +{ + __u32 logon_id; // uid __u32 process_id; __u32 is_root; __u32 destination_ipv4; @@ -70,7 +77,9 @@ struct gpa_sock_addr_local_entry { // Compile-time layout assertions to guarantee binary compatibility with Rust loader. // If these fail, the Rust [u32; N] mappings in ebpf_obj.rs must be updated too. +_Static_assert(sizeof(struct gpa_ip_address) == 16, "ip_address must be 16 bytes ([u32; 4])"); _Static_assert(sizeof(struct gpa_destination_entry) == 24, "destination_entry must be 24 bytes ([u32; 6])"); _Static_assert(sizeof(struct gpa_audit_key) == 8, "audit_key must be 8 bytes ([u32; 2])"); _Static_assert(sizeof(struct gpa_audit_event) == 20, "audit_event must be 20 bytes ([u32; 5])"); _Static_assert(sizeof(struct gpa_skip_process_entry) == 4, "skip_process_entry must be 4 bytes ([u32; 1])"); +_Static_assert(sizeof(struct gpa_sock_addr_local_entry) == 24, "sock_addr_local_entry must be 24 bytes ([u32; 6])");