From 5c1c202bfdfe30ae1a5c091ab41c5418baf93fc6 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 17 Jun 2026 21:57:46 +0000 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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])"); From ee2424f113df36cdf3febc00a3e457a1838ce0a2 Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Tue, 7 Jul 2026 14:33:39 -0700 Subject: [PATCH 11/17] unify the user mode audit entry in rust too --- proxy_agent/src/redirector.rs | 2 + proxy_agent/src/redirector/linux.rs | 9 +- proxy_agent/src/redirector/linux/ebpf_obj.rs | 221 --------- proxy_agent/src/redirector/shared_ebpf.rs | 443 ++++++++++++++++++ .../src/redirector/shared_ebpf_tests.rs | 65 +++ proxy_agent/src/redirector/windows/bpf_obj.rs | 273 +---------- 6 files changed, 519 insertions(+), 494 deletions(-) delete mode 100644 proxy_agent/src/redirector/linux/ebpf_obj.rs create mode 100644 proxy_agent/src/redirector/shared_ebpf.rs create mode 100644 proxy_agent/src/redirector/shared_ebpf_tests.rs diff --git a/proxy_agent/src/redirector.rs b/proxy_agent/src/redirector.rs index 2dc87a46..e94f1ee1 100644 --- a/proxy_agent/src/redirector.rs +++ b/proxy_agent/src/redirector.rs @@ -45,6 +45,8 @@ mod windows; #[cfg(not(windows))] mod linux; +mod shared_ebpf; + use crate::common::error::BpfErrorType; use crate::common::error::Error; use crate::common::helpers; diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index cf5e1776..3059b152 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT -mod ebpf_obj; - use crate::common::{ config, constants, error::{BpfErrorType, Error}, @@ -16,7 +14,7 @@ use aya::{ programs::CgroupAttachMode, }; use aya::{Btf, Ebpf, EbpfLoader}; -use ebpf_obj::{ +use crate::redirector::shared_ebpf::linux_types::{ destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, }; use proxy_agent_shared::telemetry::event_logger; @@ -488,8 +486,9 @@ pub async fn update_hostga_redirect_policy( mod tests { use crate::common::config; use crate::common::constants; - use crate::redirector::linux::ebpf_obj::sock_addr_audit_entry; - use crate::redirector::linux::ebpf_obj::sock_addr_audit_key; + use crate::redirector::shared_ebpf::linux_types::{ + sock_addr_audit_entry, sock_addr_audit_key, + }; use aya::maps::HashMap; use proxy_agent_shared::misc_helpers; use std::env; diff --git a/proxy_agent/src/redirector/linux/ebpf_obj.rs b/proxy_agent/src/redirector/linux/ebpf_obj.rs deleted file mode 100644 index 706b2eaf..00000000 --- a/proxy_agent/src/redirector/linux/ebpf_obj.rs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation -// SPDX-License-Identifier: MIT -#![allow(non_camel_case_types)] - -#[repr(C)] -pub struct _ip_address { - //pub ipv4: u32, - pub ip: [u32; 4], // ipv4 uses the first element; ipv6 uses all 4 elements -} -impl _ip_address { - fn empty() -> Self { - _ip_address { ip: [0, 0, 0, 0] } - } - - pub fn from_ipv4(ipv4: u32) -> Self { - let mut ip = Self::empty(); - ip.ip[0] = ipv4; - ip - } - - #[allow(dead_code)] - pub fn from_ipv6(ipv6: [u32; 4]) -> Self { - let mut ip = Self::empty(); - ip.ip.copy_from_slice(&ipv6); - ip - } -} -pub type ip_address = _ip_address; - -#[repr(C)] -pub struct _destination_entry { - pub destination_ip: ip_address, - pub destination_port: u32, // first element is the port number, second element is empty - pub protocol: u32, -} -impl _destination_entry { - pub fn empty() -> Self { - _destination_entry { - destination_ip: ip_address::empty(), - destination_port: 0, - protocol: IPPROTO_TCP, - } - } - - pub fn from_ipv4(ipv4: u32, port: u16) -> Self { - let mut entry = Self::empty(); - entry.destination_ip = ip_address::from_ipv4(ipv4); - entry.destination_port = port.to_be() as u32; - entry - } - - pub fn to_array(&self) -> [u32; 6] { - let mut array: [u32; 6] = [0; 6]; - array[..4].copy_from_slice(&self.destination_ip.ip); - array[4] = self.destination_port; - array[5] = self.protocol; - array - } -} -pub type destination_entry = _destination_entry; -pub const IPPROTO_TCP: u32 = 6; - -#[repr(C)] -pub struct sock_addr_skip_process_entry { - pub pid: u32, -} - -impl sock_addr_skip_process_entry { - fn empty() -> Self { - sock_addr_skip_process_entry { pid: 0 } - } - pub fn from_pid(pid: u32) -> Self { - let mut entry = Self::empty(); - entry.pid = pid; - entry - } - pub fn to_array(&self) -> [u32; 1] { - let mut array: [u32; 1] = [0; 1]; - array[0] = self.pid; - array - } -} - -#[repr(C)] -#[derive(Debug)] -pub struct sock_addr_audit_key { - pub protocol: u32, - pub source_port: u32, -} -#[allow(dead_code)] -impl sock_addr_audit_key { - pub fn from_source_port(port: u16) -> Self { - sock_addr_audit_key { - protocol: IPPROTO_TCP, - source_port: port as u32, - } - } - - pub fn to_array(&self) -> [u32; 2] { - let mut array: [u32; 2] = [0; 2]; - array[0] = self.protocol; - array[1] = self.source_port; - array - } - - pub fn from_array(array: [u32; 2]) -> Self { - sock_addr_audit_key { - protocol: array[0], - source_port: array[1], - } - } -} - -#[repr(C)] -#[derive(Debug)] -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, -} - -#[allow(dead_code)] -impl sock_addr_audit_entry { - pub fn from_array(array: [u32; 5]) -> Self { - sock_addr_audit_entry { - logon_id: array[0], - process_id: array[1], - is_root: array[2], - destination_ipv4: array[3], - destination_port: array[4], - } - } - - #[allow(dead_code)] - pub fn to_array(&self) -> [u32; 5] { - let mut array: [u32; 5] = [0; 5]; - array[0] = self.logon_id; - array[1] = self.process_id; - array[2] = self.is_root; - array[3] = self.destination_ipv4; - array[4] = self.destination_port; - array - } -} - -#[cfg(test)] -mod tests { - use crate::common::constants; - - #[test] - fn destination_entry_test() { - let key = super::destination_entry::from_ipv4( - constants::WIRE_SERVER_IP_NETWORK_BYTE_ORDER, - constants::WIRE_SERVER_PORT, - ); - - let array = key.to_array(); - assert_eq!( - array[0], - constants::WIRE_SERVER_IP_NETWORK_BYTE_ORDER, - "ip is not equal" - ); - assert_eq!( - array[4], - constants::WIRE_SERVER_PORT.to_be() as u32, - "port is not equal" - ); - } - - #[test] - fn sock_addr_skip_process_entry_test() { - let pid = std::process::id(); - let key = super::sock_addr_skip_process_entry::from_pid(pid); - let array = key.to_array(); - assert_eq!(array[0], pid, "pid is not equal"); - } - - #[test] - fn sock_addr_audit_key_test() { - let source_port = 1234; - let key = super::sock_addr_audit_key::from_source_port(source_port); - let array = key.to_array(); - assert_eq!(array[1], source_port as u32, "port is not equal"); - let key2 = super::sock_addr_audit_key::from_array(array); - assert_eq!( - key2.source_port, source_port as u32, - "port is not equal from_array" - ); - } - - #[test] - fn sock_addr_audit_entry_test() { - let audit = super::sock_addr_audit_entry { - logon_id: 1, - process_id: 2, - is_root: 1, - destination_ipv4: 4, - destination_port: 5, - }; - let audit_value = super::sock_addr_audit_entry::from_array(audit.to_array()); - assert_eq!( - audit_value.logon_id, audit.logon_id, - "logon_id is not equal" - ); - assert_eq!( - audit_value.process_id, audit.process_id, - "process_id is not equal" - ); - assert_eq!(audit_value.is_root, audit.is_root, "is_root is not equal"); - assert_eq!( - audit_value.destination_ipv4, audit.destination_ipv4, - "destination_ipv4 is not equal" - ); - assert_eq!( - audit_value.destination_port, audit.destination_port, - "destination_port is not equal" - ); - } -} diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs new file mode 100644 index 00000000..a2996d49 --- /dev/null +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MIT +#![allow(non_camel_case_types)] +#![cfg_attr(windows, allow(dead_code))] + +use crate::common::{ + error::{BpfErrorType, Error}, + result::Result, +}; +use std::ffi::c_void; + +#[repr(C)] +pub struct _ip_address { + pub ip: [u32; 4], // ipv4 uses the first element; ipv6 uses all 4 elements +} +impl _ip_address { + fn empty() -> Self { + _ip_address { ip: [0, 0, 0, 0] } + } + + pub fn from_ipv4(ipv4: u32) -> Self { + let mut ip = Self::empty(); + ip.ip[0] = ipv4; + ip + } + + #[allow(dead_code)] + pub fn from_ipv6(ipv6: [u32; 4]) -> Self { + let mut ip = Self::empty(); + ip.ip.copy_from_slice(&ipv6); + ip + } +} +pub type ip_address = _ip_address; + +#[repr(C)] +pub struct _destination_entry { + pub destination_ip: ip_address, + pub destination_port: u32, + pub protocol: u32, +} +impl _destination_entry { + pub fn empty() -> Self { + _destination_entry { + destination_ip: ip_address::empty(), + destination_port: 0, + protocol: IPPROTO_TCP, + } + } + + pub fn from_ipv4(ipv4: u32, port: u16) -> Self { + let mut entry = Self::empty(); + entry.destination_ip = ip_address::from_ipv4(ipv4); + entry.destination_port = u32::from(port.to_be()); + entry + } + + pub fn to_array(&self) -> [u32; 6] { + let mut array: [u32; 6] = [0; 6]; + array[..4].copy_from_slice(&self.destination_ip.ip); + array[4] = self.destination_port; + array[5] = self.protocol; + array + } +} +pub type destination_entry = _destination_entry; + +pub const IPPROTO_TCP: u32 = 6; +#[allow(dead_code)] +pub const IPPROTO_UDP: u32 = 17; + +#[repr(C)] +pub struct sock_addr_skip_process_entry { + pub pid: u32, +} +impl sock_addr_skip_process_entry { + fn empty() -> Self { + sock_addr_skip_process_entry { pid: 0 } + } + + pub fn from_pid(pid: u32) -> Self { + let mut entry = Self::empty(); + entry.pid = pid; + entry + } + + pub fn to_array(&self) -> [u32; 1] { + [self.pid] + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct sock_addr_audit_key { + pub protocol: u32, + pub source_port: u32, +} +#[allow(dead_code)] +impl sock_addr_audit_key { + #[cfg(windows)] + pub fn from_source_port(port: u16) -> Self { + sock_addr_audit_key { + protocol: IPPROTO_TCP, + source_port: u32::from(port.to_be()), + } + } + + #[cfg(not(windows))] + pub fn from_source_port(port: u16) -> Self { + sock_addr_audit_key { + protocol: IPPROTO_TCP, + source_port: u32::from(port), + } + } + + pub fn to_array(&self) -> [u32; 2] { + [self.protocol, self.source_port] + } + + pub fn from_array(array: [u32; 2]) -> Self { + sock_addr_audit_key { + protocol: array[0], + source_port: array[1], + } + } +} + +#[repr(C)] +#[derive(Debug)] +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, +} +impl sock_addr_audit_entry { + pub fn empty() -> Self { + sock_addr_audit_entry { + logon_id: 0, + process_id: 0, + is_root: 0, + destination_ipv4: 0, + destination_port: 0, + } + } + + pub fn from_array(array: [u32; 5]) -> Self { + sock_addr_audit_entry { + logon_id: array[0], + process_id: array[1], + is_root: array[2], + destination_ipv4: array[3], + destination_port: array[4], + } + } + + #[allow(dead_code)] + pub fn to_array(&self) -> [u32; 5] { + [ + self.logon_id, + self.process_id, + self.is_root, + self.destination_ipv4, + self.destination_port, + ] + } + + pub fn to_audit_entry(&self) -> crate::redirector::AuditEntry { + crate::redirector::AuditEntry { + logon_id: u64::from(self.logon_id), + process_id: self.process_id, + is_admin: self.is_root as i32, + destination_ipv4: self.destination_ipv4, + destination_port: self.destination_port as u16, + } + } +} + +/// Legacy eBPF audit entry layout used by older Windows eBPF programs. +#[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, +} +impl sock_addr_audit_entry_legacy { + pub fn empty() -> Self { + sock_addr_audit_entry_legacy { + logon_id: 0, + process_id: 0, + is_admin: 0, + destination_ipv4: 0, + 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), + Legacy(sock_addr_audit_entry_legacy), + // 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; + + /// Returns the maximum value size among all supported 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::empty()), + Self::VALUE_SIZE_LEGACY => { + AuditValueEntry::Legacy(sock_addr_audit_entry_legacy::empty()) + } + _ => { + let max_value_size = Self::max_value_size(); + AuditValueEntry::Unknown(GenericAuditValueEntry { + // Allocate empty buffer with the maximum value size 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 as *mut c_void, + AuditValueEntry::Legacy(entry) => { + entry as *mut sock_addr_audit_entry_legacy 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 raw bytes to concrete audit value type based on 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( + "to_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, + ) + }; + 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( + "to_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, + ) + }; + Ok(value.to_audit_entry()) + } + _ => Err(Error::Bpf(BpfErrorType::MapLookupElem( + "to_audit_entry".to_string(), + format!( + "Invalid audit value size read: {}, allocated: {}", + entry.read_size, entry.data_size + ), + ))), + } + } + } + } +} + +#[cfg(not(windows))] +pub mod linux_types { + pub use super::{ + destination_entry, ip_address, sock_addr_audit_entry, sock_addr_audit_key, + sock_addr_skip_process_entry, IPPROTO_TCP, + }; +} + +#[cfg(windows)] +pub mod windows_types { + pub use super::{ + destination_entry as destination_entry_t, sock_addr_audit_key as sock_addr_audit_key_t, + sock_addr_skip_process_entry, + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn destination_entry_ipv4_roundtrip_array_shape() { + let entry = destination_entry::from_ipv4(0x1081_3FA8, 80); + let array = entry.to_array(); + + assert_eq!(array[0], 0x1081_3FA8, "destination ipv4 should be in slot 0"); + assert_eq!(array[4], u32::from(80u16.to_be()), "port should be network byte order"); + assert_eq!(array[5], IPPROTO_TCP, "protocol should be TCP by default"); + } + + #[test] + fn audit_key_array_roundtrip() { + let key = sock_addr_audit_key::from_source_port(1234); + let array = key.to_array(); + let rebuilt = sock_addr_audit_key::from_array(array); + + assert_eq!(rebuilt.protocol, IPPROTO_TCP, "protocol mismatch"); + + #[cfg(windows)] + assert_eq!(rebuilt.source_port, u32::from(1234u16.to_be()), "source_port mismatch"); + + #[cfg(not(windows))] + assert_eq!(rebuilt.source_port, 1234u32, "source_port mismatch"); + } + + #[test] + fn skip_process_entry_pid_roundtrip() { + let pid = 4321; + let key = sock_addr_skip_process_entry::from_pid(pid); + + assert_eq!(key.to_array(), [pid], "pid should roundtrip through the map key layout"); + } + + #[test] + fn audit_entry_canonical_array_roundtrip() { + let canonical = sock_addr_audit_entry { + logon_id: 1, + process_id: 2, + is_root: 1, + destination_ipv4: 4, + destination_port: 5, + }; + + let rebuilt = sock_addr_audit_entry::from_array(canonical.to_array()); + + assert_eq!(rebuilt.logon_id, canonical.logon_id); + assert_eq!(rebuilt.process_id, canonical.process_id); + assert_eq!(rebuilt.is_root, canonical.is_root); + assert_eq!(rebuilt.destination_ipv4, canonical.destination_ipv4); + assert_eq!(rebuilt.destination_port, canonical.destination_port); + } + + #[test] + fn audit_entry_canonical_to_agent_audit_entry() { + let canonical = sock_addr_audit_entry { + logon_id: 42, + process_id: 1000, + is_root: 1, + destination_ipv4: 0x0102_0304, + destination_port: u32::from(8080u16.to_be()), + }; + + let audit = canonical.to_audit_entry(); + assert_eq!(audit.logon_id, 42); + assert_eq!(audit.process_id, 1000); + assert_eq!(audit.is_admin, 1); + assert_eq!(audit.destination_ipv4, 0x0102_0304); + assert_eq!(audit.destination_port, 8080u16.to_be()); + } + + #[test] + fn audit_entry_legacy_to_agent_audit_entry() { + let legacy = sock_addr_audit_entry_legacy { + logon_id: 7, + process_id: 2000, + is_admin: 0, + destination_ipv4: 0xAABB_CCDD, + destination_port: 53, + }; + + let audit = legacy.to_audit_entry(); + assert_eq!(audit.logon_id, 7); + assert_eq!(audit.process_id, 2000); + assert_eq!(audit.is_admin, 0); + assert_eq!(audit.destination_ipv4, 0xAABB_CCDD); + assert_eq!(audit.destination_port, 53); + } +} diff --git a/proxy_agent/src/redirector/shared_ebpf_tests.rs b/proxy_agent/src/redirector/shared_ebpf_tests.rs new file mode 100644 index 00000000..0b1b9328 --- /dev/null +++ b/proxy_agent/src/redirector/shared_ebpf_tests.rs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MIT + +use super::shared_ebpf; + +#[test] +fn destination_entry_ipv4_roundtrip_array_shape() { + let entry = shared_ebpf::destination_entry::from_ipv4(0x1081_3FA8, 80); + let array = entry.to_array(); + + assert_eq!(array[0], 0x1081_3FA8, "destination ipv4 should be in slot 0"); + assert_eq!(array[4], u32::from(80u16.to_be()), "port should be network byte order"); + assert_eq!(array[5], shared_ebpf::IPPROTO_TCP, "protocol should be TCP by default"); +} + +#[test] +fn audit_key_array_roundtrip() { + let key = shared_ebpf::sock_addr_audit_key::from_source_port(1234); + let array = key.to_array(); + let rebuilt = shared_ebpf::sock_addr_audit_key::from_array(array); + + assert_eq!(rebuilt.protocol, shared_ebpf::IPPROTO_TCP, "protocol mismatch"); + + #[cfg(windows)] + assert_eq!(rebuilt.source_port, u32::from(1234u16.to_be()), "source_port mismatch"); + + #[cfg(not(windows))] + assert_eq!(rebuilt.source_port, 1234u32, "source_port mismatch"); +} + +#[test] +fn audit_entry_canonical_to_agent_audit_entry() { + let canonical = shared_ebpf::sock_addr_audit_entry { + logon_id: 42, + process_id: 1000, + is_root: 1, + destination_ipv4: 0x0102_0304, + destination_port: u32::from(8080u16.to_be()), + }; + + let audit = canonical.to_audit_entry(); + assert_eq!(audit.logon_id, 42); + assert_eq!(audit.process_id, 1000); + assert_eq!(audit.is_admin, 1); + assert_eq!(audit.destination_ipv4, 0x0102_0304); + assert_eq!(audit.destination_port, 8080u16.to_be()); +} + +#[test] +fn audit_entry_legacy_to_agent_audit_entry() { + let legacy = shared_ebpf::sock_addr_audit_entry_legacy { + logon_id: 7, + process_id: 2000, + is_admin: 0, + destination_ipv4: 0xAABB_CCDD, + destination_port: 53, + }; + + let audit = legacy.to_audit_entry(); + assert_eq!(audit.logon_id, 7); + assert_eq!(audit.process_id, 2000); + assert_eq!(audit.is_admin, 0); + assert_eq!(audit.destination_ipv4, 0xAABB_CCDD); + assert_eq!(audit.destination_port, 53); +} diff --git a/proxy_agent/src/redirector/windows/bpf_obj.rs b/proxy_agent/src/redirector/windows/bpf_obj.rs index 8244d6e2..3b70f8b4 100644 --- a/proxy_agent/src/redirector/windows/bpf_obj.rs +++ b/proxy_agent/src/redirector/windows/bpf_obj.rs @@ -2,12 +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 use crate::redirector::shared_ebpf::windows_types::{ + destination_entry_t, sock_addr_audit_key_t, sock_addr_skip_process_entry, +}; +pub(crate) use crate::redirector::shared_ebpf::AuditValueEntry; pub type ebpf_id_t = u32; pub type fd_t = i32; @@ -226,269 +226,6 @@ pub struct bpf_object { execution_type: ebpf_execution_type_t, } -#[repr(C)] -pub struct _sock_addr_audit_key { - pub protocol: u32, - 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: 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 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 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. -#[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, - } - } - - 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)] -pub struct _ip_address { - //pub ipv4: u32, - pub ip: [u32; 4], // ipv4 uses the first element; ipv6 uses all 4 elements -} -impl _ip_address { - fn empty() -> Self { - _ip_address { ip: [0, 0, 0, 0] } - } - - pub fn from_ipv4(ipv4: u32) -> Self { - let mut ip = Self::empty(); - ip.ip[0] = ipv4; - ip - } - - #[allow(dead_code)] - pub fn from_ipv6(ipv6: [u32; 4]) -> Self { - let mut ip = Self::empty(); - ip.ip.copy_from_slice(&ipv6); - ip - } -} -pub type ip_address_t = _ip_address; - -#[repr(C)] -pub struct _destination_entry { - pub destination_ip: ip_address_t, - 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, - protocol: IPPROTO_TCP, - } - } - - 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 = u32::from(port.to_be()); - entry - } -} - -pub type destination_entry_t = _destination_entry; -pub const IPPROTO_TCP: u32 = 6; -#[allow(dead_code)] -pub const IPPROTO_UDP: u32 = 17; - -#[repr(C)] -pub struct _sock_addr_skip_process_entry { - pub pid: u32, -} -pub type sock_addr_skip_process_entry = _sock_addr_skip_process_entry; From 5834bdd1f7b127b77caf9ff1a83f39eb159fb87b Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Tue, 7 Jul 2026 14:36:41 -0700 Subject: [PATCH 12/17] Update format and md file --- doc/plans/Innovation-4.2-core-unify-ebpf.md | 18 +++++++------- proxy_agent/src/redirector/linux.rs | 10 +++----- proxy_agent/src/redirector/shared_ebpf.rs | 27 ++++++++++++++++----- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/doc/plans/Innovation-4.2-core-unify-ebpf.md b/doc/plans/Innovation-4.2-core-unify-ebpf.md index 33bb7031..da3b2f9d 100644 --- a/doc/plans/Innovation-4.2-core-unify-ebpf.md +++ b/doc/plans/Innovation-4.2-core-unify-ebpf.md @@ -59,11 +59,11 @@ 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/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. +- 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 shared Rust decoder in `proxy_agent/src/redirector/shared_ebpf.rs` accepts both the canonical layout and the legacy layout to preserve compatibility with previously shipped eBPF programs. ## 4. Shared Headers -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. +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 shared Rust model** in `proxy_agent/src/redirector/shared_ebpf.rs`, which maps each to a fixed-size `[u32; N]` array and exports Linux/Windows-specific aliases from one place — so field order/size cannot change without updating both sides. // shared-ebpf/include/gpa_audit_event.h #pragma once @@ -91,7 +91,7 @@ The canonical, platform-neutral structs live in `shared-ebpf/include/gpa_audit_e - 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`, ...). +- The matching Rust layout is asserted by the `redirector::shared_ebpf` unit tests (`destination_entry_ipv4_roundtrip_array_shape`, `skip_process_entry_pid_roundtrip`, `audit_entry_canonical_array_roundtrip`, ...). ## 5. Build System @@ -118,16 +118,16 @@ Windows eBPF objects are compiled by **`build.cmd`** (`redirect.bpf.o` then `red ## 7. Integration -- `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 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. +- `proxy_agent/src/redirector/linux.rs` loads the object with **aya** (`EbpfLoader` + BTF), attaches the two programs by name (`connect4` via `CgroupSockAddr`, `tcp_v4_connect` via `KProbe` on `tcp_connect`), and imports its map key/value layouts directly from `proxy_agent/src/redirector/shared_ebpf.rs`. +- `proxy_agent/src/redirector/shared_ebpf.rs` now owns the `[u32; N]` ↔ `gpa_*` binary contract for map keys/values plus the shared audit decode path used by both Linux and Windows. +- Windows side uses the shared header and shared dual-decode compatibility in user-space: `lookup_audit` and redirect-context parsing decode canonical `sock_addr_audit_entry` values when present and fall back to `sock_addr_audit_entry_legacy` 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::shared_ebpf` layout unit tests assert the Rust `[u32; N]` mappings match the shared C structs, including Linux key/value roundtrips and Windows-compatible audit decode paths (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; dedicated Windows compatibility tests that exercise both canonical and legacy audit layouts. +- Pending: cross-kernel matrix CI (5.4, 5.15, 6.1, 6.8) loading the same object; load-time budget \< 100 ms; broader Windows compatibility tests that exercise both canonical and legacy audit layouts through the live map and redirect-context paths. ## 9. Risks @@ -142,7 +142,7 @@ Windows eBPF objects are compiled by **`build.cmd`** (`redirect.bpf.o` then `red | M | Deliverable | Exit | |-----|----------------------------------------------|------------------------------------------------------| | 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 | +| M2 | Shared header + Rust layout assertions | ✅ `shared_ebpf` layout tests green on Linux and shared decode path in place | | M3 | Arch-aware, fail-fast, deterministic build | ✅ `build.rs` (x86_64 + arm64) owns the compile | | 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 | diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index 3059b152..36571e4e 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -6,6 +6,9 @@ use crate::common::{ logger, result::Result, }; +use crate::redirector::shared_ebpf::linux_types::{ + destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, +}; use crate::redirector::{ip_to_string, AuditEntry}; use crate::shared_state::redirector_wrapper::RedirectorSharedState; use aya::programs::{CgroupSockAddr, KProbe}; @@ -14,9 +17,6 @@ use aya::{ programs::CgroupAttachMode, }; use aya::{Btf, Ebpf, EbpfLoader}; -use crate::redirector::shared_ebpf::linux_types::{ - destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, -}; use proxy_agent_shared::telemetry::event_logger; use proxy_agent_shared::{logger::LoggerLevel, misc_helpers}; use std::convert::TryFrom; @@ -486,9 +486,7 @@ pub async fn update_hostga_redirect_policy( mod tests { use crate::common::config; use crate::common::constants; - use crate::redirector::shared_ebpf::linux_types::{ - sock_addr_audit_entry, sock_addr_audit_key, - }; + use crate::redirector::shared_ebpf::linux_types::{sock_addr_audit_entry, sock_addr_audit_key}; use aya::maps::HashMap; use proxy_agent_shared::misc_helpers; use std::env; diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index a2996d49..be14f196 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -296,7 +296,7 @@ impl AuditValueEntry { let value = unsafe { std::ptr::read_unaligned( - entry.buffer.as_ptr() as *const sock_addr_audit_entry, + entry.buffer.as_ptr() as *const sock_addr_audit_entry ) }; Ok(value.to_audit_entry()) @@ -315,7 +315,7 @@ impl AuditValueEntry { let value = unsafe { std::ptr::read_unaligned( - entry.buffer.as_ptr() as *const sock_addr_audit_entry_legacy, + entry.buffer.as_ptr() as *const sock_addr_audit_entry_legacy ) }; Ok(value.to_audit_entry()) @@ -358,8 +358,15 @@ mod tests { let entry = destination_entry::from_ipv4(0x1081_3FA8, 80); let array = entry.to_array(); - assert_eq!(array[0], 0x1081_3FA8, "destination ipv4 should be in slot 0"); - assert_eq!(array[4], u32::from(80u16.to_be()), "port should be network byte order"); + assert_eq!( + array[0], 0x1081_3FA8, + "destination ipv4 should be in slot 0" + ); + assert_eq!( + array[4], + u32::from(80u16.to_be()), + "port should be network byte order" + ); assert_eq!(array[5], IPPROTO_TCP, "protocol should be TCP by default"); } @@ -372,7 +379,11 @@ mod tests { assert_eq!(rebuilt.protocol, IPPROTO_TCP, "protocol mismatch"); #[cfg(windows)] - assert_eq!(rebuilt.source_port, u32::from(1234u16.to_be()), "source_port mismatch"); + assert_eq!( + rebuilt.source_port, + u32::from(1234u16.to_be()), + "source_port mismatch" + ); #[cfg(not(windows))] assert_eq!(rebuilt.source_port, 1234u32, "source_port mismatch"); @@ -383,7 +394,11 @@ mod tests { let pid = 4321; let key = sock_addr_skip_process_entry::from_pid(pid); - assert_eq!(key.to_array(), [pid], "pid should roundtrip through the map key layout"); + assert_eq!( + key.to_array(), + [pid], + "pid should roundtrip through the map key layout" + ); } #[test] From 6ca2b884de3f74d4b38493f7f0e4268e5e9bac8b Mon Sep 17 00:00:00 2001 From: "Zhidong Peng (HE/HIM)" Date: Tue, 7 Jul 2026 14:37:35 -0700 Subject: [PATCH 13/17] remove file --- .../src/redirector/shared_ebpf_tests.rs | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 proxy_agent/src/redirector/shared_ebpf_tests.rs diff --git a/proxy_agent/src/redirector/shared_ebpf_tests.rs b/proxy_agent/src/redirector/shared_ebpf_tests.rs deleted file mode 100644 index 0b1b9328..00000000 --- a/proxy_agent/src/redirector/shared_ebpf_tests.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation -// SPDX-License-Identifier: MIT - -use super::shared_ebpf; - -#[test] -fn destination_entry_ipv4_roundtrip_array_shape() { - let entry = shared_ebpf::destination_entry::from_ipv4(0x1081_3FA8, 80); - let array = entry.to_array(); - - assert_eq!(array[0], 0x1081_3FA8, "destination ipv4 should be in slot 0"); - assert_eq!(array[4], u32::from(80u16.to_be()), "port should be network byte order"); - assert_eq!(array[5], shared_ebpf::IPPROTO_TCP, "protocol should be TCP by default"); -} - -#[test] -fn audit_key_array_roundtrip() { - let key = shared_ebpf::sock_addr_audit_key::from_source_port(1234); - let array = key.to_array(); - let rebuilt = shared_ebpf::sock_addr_audit_key::from_array(array); - - assert_eq!(rebuilt.protocol, shared_ebpf::IPPROTO_TCP, "protocol mismatch"); - - #[cfg(windows)] - assert_eq!(rebuilt.source_port, u32::from(1234u16.to_be()), "source_port mismatch"); - - #[cfg(not(windows))] - assert_eq!(rebuilt.source_port, 1234u32, "source_port mismatch"); -} - -#[test] -fn audit_entry_canonical_to_agent_audit_entry() { - let canonical = shared_ebpf::sock_addr_audit_entry { - logon_id: 42, - process_id: 1000, - is_root: 1, - destination_ipv4: 0x0102_0304, - destination_port: u32::from(8080u16.to_be()), - }; - - let audit = canonical.to_audit_entry(); - assert_eq!(audit.logon_id, 42); - assert_eq!(audit.process_id, 1000); - assert_eq!(audit.is_admin, 1); - assert_eq!(audit.destination_ipv4, 0x0102_0304); - assert_eq!(audit.destination_port, 8080u16.to_be()); -} - -#[test] -fn audit_entry_legacy_to_agent_audit_entry() { - let legacy = shared_ebpf::sock_addr_audit_entry_legacy { - logon_id: 7, - process_id: 2000, - is_admin: 0, - destination_ipv4: 0xAABB_CCDD, - destination_port: 53, - }; - - let audit = legacy.to_audit_entry(); - assert_eq!(audit.logon_id, 7); - assert_eq!(audit.process_id, 2000); - assert_eq!(audit.is_admin, 0); - assert_eq!(audit.destination_ipv4, 0xAABB_CCDD); - assert_eq!(audit.destination_port, 53); -} From 2d69a3de7ca3eb346b51b4b949ab4fda1e01d261 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Tue, 7 Jul 2026 21:45:28 +0000 Subject: [PATCH 14/17] fix linux build issue --- proxy_agent/src/redirector/shared_ebpf.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index be14f196..3b23df78 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT #![allow(non_camel_case_types)] -#![cfg_attr(windows, allow(dead_code))] +#![allow(dead_code)] use crate::common::{ error::{BpfErrorType, Error}, @@ -336,8 +336,7 @@ impl AuditValueEntry { #[cfg(not(windows))] pub mod linux_types { pub use super::{ - destination_entry, ip_address, sock_addr_audit_entry, sock_addr_audit_key, - sock_addr_skip_process_entry, IPPROTO_TCP, + destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, }; } From 6e9d38dfb8c93eb5c7cbc49b0df88a663ee5320e Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 8 Jul 2026 16:07:17 -0700 Subject: [PATCH 15/17] set value size returned by WSAIoctl --- proxy_agent/src/redirector/shared_ebpf.rs | 25 +++++++++++++++++++++++ proxy_agent/src/redirector/windows.rs | 3 +++ 2 files changed, 28 insertions(+) diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index 3b23df78..67b33483 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -257,6 +257,31 @@ impl AuditValueEntry { } } + pub fn set_value_size(&mut self, value_size: u32) -> Result<()> { + match self { + AuditValueEntry::Unknown(entry) => { + // check the value size is valid for the current entry types + if value_size != Self::VALUE_SIZE_NEW && value_size != Self::VALUE_SIZE_LEGACY { + return Err(Error::Bpf(BpfErrorType::MapLookupElem( + "set_value_size".to_string(), + format!( + "Invalid audit value size: {}, expected: {} or {}", + value_size, + Self::VALUE_SIZE_NEW, + Self::VALUE_SIZE_LEGACY, + ), + ))); + } + + entry.read_size = value_size; + } + _ => { + // No-op, value size is fixed for other known types. + } + } + Ok(()) + } + pub fn value_size(&self) -> u32 { match self { AuditValueEntry::New(_) => Self::VALUE_SIZE_NEW, diff --git a/proxy_agent/src/redirector/windows.rs b/proxy_agent/src/redirector/windows.rs index c9238124..e1b214fb 100644 --- a/proxy_agent/src/redirector/windows.rs +++ b/proxy_agent/src/redirector/windows.rs @@ -129,6 +129,9 @@ pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result Date: Wed, 8 Jul 2026 16:23:01 -0700 Subject: [PATCH 16/17] Add unitests for AuditValueEntry. --- proxy_agent/src/redirector/shared_ebpf.rs | 167 ++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index 67b33483..01e24eff 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -479,4 +479,171 @@ mod tests { assert_eq!(audit.destination_ipv4, 0xAABB_CCDD); assert_eq!(audit.destination_port, 53); } + + /// Copies the raw bytes of an audit value into an Unknown entry's buffer. + fn fill_unknown_buffer(entry: &mut AuditValueEntry, value: &T, size: u32) { + if let AuditValueEntry::Unknown(unknown) = entry { + let bytes = + unsafe { std::slice::from_raw_parts(value as *const T as *const u8, size as usize) }; + unknown.buffer[..bytes.len()].copy_from_slice(bytes); + } else { + panic!("expected Unknown variant"); + } + } + + #[test] + fn audit_value_entry_empty_selects_expected_variant() { + // Canonical size -> New. + let new_entry = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW); + assert!(matches!(new_entry, AuditValueEntry::New(_))); + assert_eq!(new_entry.value_size(), AuditValueEntry::VALUE_SIZE_NEW); + + // Legacy size -> Legacy. + let legacy_entry = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_LEGACY); + assert!(matches!(legacy_entry, AuditValueEntry::Legacy(_))); + assert_eq!(legacy_entry.value_size(), AuditValueEntry::VALUE_SIZE_LEGACY); + + // Any other size -> Unknown, buffer allocated to the max value size. + let odd_size = AuditValueEntry::VALUE_SIZE_NEW + 1; + let unknown_entry = AuditValueEntry::empty(odd_size); + let max_value_size = AuditValueEntry::max_value_size(); + match &unknown_entry { + AuditValueEntry::Unknown(unknown) => { + assert_eq!(unknown.buffer.len(), max_value_size as usize); + assert_eq!(unknown.data_size, max_value_size); + assert_eq!(unknown.read_size, odd_size); + } + _ => panic!("unexpected variant for unknown value size"), + } + // value_size() returns the allocated data_size for unknown entries. + assert_eq!(unknown_entry.value_size(), max_value_size); + + // value_pointer_mut() is non-null for every variant. + let mut new_entry = new_entry; + let mut legacy_entry = legacy_entry; + let mut unknown_entry = unknown_entry; + assert!(!new_entry.value_pointer_mut().is_null()); + assert!(!legacy_entry.value_pointer_mut().is_null()); + assert!(!unknown_entry.value_pointer_mut().is_null()); + } + + #[test] + fn audit_value_entry_set_value_size() { + // Valid size updates read_size for Unknown entries. + let mut unknown = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); + unknown + .set_value_size(AuditValueEntry::VALUE_SIZE_LEGACY) + .expect("valid value size should be accepted"); + match &unknown { + AuditValueEntry::Unknown(entry) => { + assert_eq!(entry.read_size, AuditValueEntry::VALUE_SIZE_LEGACY); + } + _ => panic!("expected Unknown variant"), + } + + // Invalid size is rejected for Unknown entries. + let mut unknown = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); + assert!( + unknown + .set_value_size(AuditValueEntry::VALUE_SIZE_NEW + 1) + .is_err(), + "an invalid value size should be rejected for unknown entries" + ); + + // Any size is a no-op (and never errors) for known variants. + let mut known = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW); + known + .set_value_size(AuditValueEntry::VALUE_SIZE_NEW + 1) + .expect("set_value_size should be a no-op for known variants"); + assert!(matches!(known, AuditValueEntry::New(_))); + assert_eq!(known.value_size(), AuditValueEntry::VALUE_SIZE_NEW); + } + + #[test] + fn audit_value_entry_to_audit_entry() { + // New variant converts directly. + let new_entry = AuditValueEntry::New(sock_addr_audit_entry { + logon_id: 11, + process_id: 22, + is_root: 1, + destination_ipv4: 0x0A00_0001, + destination_port: u32::from(443u16.to_be()), + }); + let audit = new_entry.to_audit_entry().expect("New should convert"); + assert_eq!(audit.logon_id, 11); + assert_eq!(audit.process_id, 22); + assert_eq!(audit.is_admin, 1); + assert_eq!(audit.destination_ipv4, 0x0A00_0001); + assert_eq!(audit.destination_port, 443u16.to_be()); + + // Legacy variant converts directly. + let legacy_entry = AuditValueEntry::Legacy(sock_addr_audit_entry_legacy { + logon_id: 99, + process_id: 100, + is_admin: 1, + destination_ipv4: 0x0102_0304, + destination_port: 8080, + }); + let audit = legacy_entry.to_audit_entry().expect("Legacy should convert"); + assert_eq!(audit.logon_id, 99); + assert_eq!(audit.process_id, 100); + assert_eq!(audit.is_admin, 1); + assert_eq!(audit.destination_ipv4, 0x0102_0304); + assert_eq!(audit.destination_port, 8080); + + // Unknown variant decodes a canonical (New) layout from raw bytes. + let canonical = sock_addr_audit_entry { + logon_id: 5, + process_id: 6, + is_root: 1, + destination_ipv4: 0x0808_0808, + destination_port: u32::from(53u16.to_be()), + }; + let mut unknown_new = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); + fill_unknown_buffer(&mut unknown_new, &canonical, AuditValueEntry::VALUE_SIZE_NEW); + unknown_new + .set_value_size(AuditValueEntry::VALUE_SIZE_NEW) + .expect("value size should be valid"); + let audit = unknown_new + .to_audit_entry() + .expect("unknown New should decode"); + assert_eq!(audit.logon_id, 5); + assert_eq!(audit.process_id, 6); + assert_eq!(audit.is_admin, 1); + assert_eq!(audit.destination_ipv4, 0x0808_0808); + assert_eq!(audit.destination_port, 53u16.to_be()); + + // Unknown variant decodes a legacy layout from raw bytes. + let legacy = sock_addr_audit_entry_legacy { + logon_id: 77, + process_id: 88, + is_admin: 0, + destination_ipv4: 0xDEAD_BEEF, + destination_port: 22, + }; + let mut unknown_legacy = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); + fill_unknown_buffer( + &mut unknown_legacy, + &legacy, + AuditValueEntry::VALUE_SIZE_LEGACY, + ); + unknown_legacy + .set_value_size(AuditValueEntry::VALUE_SIZE_LEGACY) + .expect("value size should be valid"); + let audit = unknown_legacy + .to_audit_entry() + .expect("unknown Legacy should decode"); + assert_eq!(audit.logon_id, 77); + assert_eq!(audit.process_id, 88); + assert_eq!(audit.is_admin, 0); + assert_eq!(audit.destination_ipv4, 0xDEAD_BEEF); + assert_eq!(audit.destination_port, 22); + + // Unknown variant with an invalid read_size fails to decode. + let invalid = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); + assert!( + invalid.to_audit_entry().is_err(), + "decoding an unknown entry with an invalid read_size should fail" + ); + } } From 26cdb8682ccf82930cbb1c5e8beed578335cb120 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Wed, 8 Jul 2026 16:23:23 -0700 Subject: [PATCH 17/17] format --- proxy_agent/src/redirector/shared_ebpf.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index 01e24eff..15bee538 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -483,8 +483,9 @@ mod tests { /// Copies the raw bytes of an audit value into an Unknown entry's buffer. fn fill_unknown_buffer(entry: &mut AuditValueEntry, value: &T, size: u32) { if let AuditValueEntry::Unknown(unknown) = entry { - let bytes = - unsafe { std::slice::from_raw_parts(value as *const T as *const u8, size as usize) }; + let bytes = unsafe { + std::slice::from_raw_parts(value as *const T as *const u8, size as usize) + }; unknown.buffer[..bytes.len()].copy_from_slice(bytes); } else { panic!("expected Unknown variant"); @@ -501,7 +502,10 @@ mod tests { // Legacy size -> Legacy. let legacy_entry = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_LEGACY); assert!(matches!(legacy_entry, AuditValueEntry::Legacy(_))); - assert_eq!(legacy_entry.value_size(), AuditValueEntry::VALUE_SIZE_LEGACY); + assert_eq!( + legacy_entry.value_size(), + AuditValueEntry::VALUE_SIZE_LEGACY + ); // Any other size -> Unknown, buffer allocated to the max value size. let odd_size = AuditValueEntry::VALUE_SIZE_NEW + 1; @@ -584,7 +588,9 @@ mod tests { destination_ipv4: 0x0102_0304, destination_port: 8080, }); - let audit = legacy_entry.to_audit_entry().expect("Legacy should convert"); + let audit = legacy_entry + .to_audit_entry() + .expect("Legacy should convert"); assert_eq!(audit.logon_id, 99); assert_eq!(audit.process_id, 100); assert_eq!(audit.is_admin, 1); @@ -600,7 +606,11 @@ mod tests { destination_port: u32::from(53u16.to_be()), }; let mut unknown_new = AuditValueEntry::empty(AuditValueEntry::VALUE_SIZE_NEW + 1); - fill_unknown_buffer(&mut unknown_new, &canonical, AuditValueEntry::VALUE_SIZE_NEW); + fill_unknown_buffer( + &mut unknown_new, + &canonical, + AuditValueEntry::VALUE_SIZE_NEW, + ); unknown_new .set_value_size(AuditValueEntry::VALUE_SIZE_NEW) .expect("value size should be valid");