diff --git a/.github/workflows/bloat.yml b/.github/workflows/bloat.yml index f80e441c..b868d465 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: "10000000" # ~10 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: "10000000" 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/.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/build-linux.sh b/build-linux.sh index 9c517327..f1699283 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/build.cmd b/build.cmd index c335a323..bba3d99f 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/cspell.json b/cspell.json index d81b4a0f..a509f9d1 100644 --- a/cspell.json +++ b/cspell.json @@ -298,8 +298,10 @@ "linkid", "llc", "llvmorg", + "loadall", "logdir", "logon", + "lossily", "Lrs", "lsa", "lsm", @@ -327,6 +329,7 @@ "msc", "msrc", "MSRs", + "multiarch", "multilib", "nbf", "ncurl", @@ -359,6 +362,7 @@ "ntimeout", "ntohs", "NTSTATUS", + "objdump", "OICI", "OIDC", "onebranch", @@ -421,6 +425,7 @@ "Razr", "rcv", "RDFE", + "recompiles", "RECVORIGDSTADDR", "redhat", "Redist", diff --git a/doc/plans/Innovation-4.2-core-unify-ebpf.md b/doc/plans/Innovation-4.2-core-unify-ebpf.md index a2fd30de..33bb7031 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/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 | Impact | Effort | Risk | Scope | @@ -39,65 +41,110 @@ 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/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 - // 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`. +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`. +- **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. + +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+. ## 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 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 -- 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; dedicated Windows compatibility tests that exercise both canonical and legacy audit layouts. ## 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. +- **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 | 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 | ✅ 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 595b3c5f..693f787c 100644 --- a/ebpf/socket.h +++ b/ebpf/socket.h @@ -1,46 +1,24 @@ // 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 +#include "../shared-ebpf/include/gpa_audit_event.h" +// 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; - -typedef struct _destination_entry -{ - ip_address_t destination_ip; - uint16_t destination_port; - uint32_t protocol; -} destination_entry_t; - -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; - -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/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..2a514de9 100644 --- a/proxy_agent/build.rs +++ b/proxy_agent/build.rs @@ -1,10 +1,168 @@ // Copyright (c) Microsoft Corporation // SPDX-License-Identifier: MIT + 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() { + 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") + .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()), + ), + // 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 + // 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/proxy_agent/src/proxy.rs b/proxy_agent/src/proxy.rs index 721e912c..918fb211 100644 --- a/proxy_agent/src/proxy.rs +++ b/proxy_agent/src/proxy.rs @@ -54,6 +54,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, @@ -62,6 +63,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, diff --git a/proxy_agent/src/redirector/windows.rs b/proxy_agent/src/redirector/windows.rs index 3f61902a..c9238124 100644 --- a/proxy_agent/src/redirector/windows.rs +++ b/proxy_agent/src/redirector/windows.rs @@ -7,10 +7,9 @@ mod bpf_prog; 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; @@ -106,9 +105,8 @@ 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; + // 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( @@ -116,29 +114,22 @@ pub fn get_audit_from_redirect_context(raw_socket_id: usize) -> Result 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 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 + ), + ))), + } + } } } } @@ -264,14 +462,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 +477,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..3a0fb5e9 100644 --- a/proxy_agent/src/redirector/windows/bpf_prog.rs +++ b/proxy_agent/src/redirector/windows/bpf_prog.rs @@ -256,24 +256,21 @@ impl BpfObject { source_port - source local port. - entry - element from audit_map. - Return Value: - 0 on success. On failure appropriate RESULT is returned. + audit entry from audit_map on success. On failure appropriate RESULT is returned. */ 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 value = AuditEntry::empty(); - + 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, - &value as *const AuditEntry as *mut c_void, + audit_value_entry.value_pointer_mut(), ) .map_err(|e| { Error::Bpf(BpfErrorType::MapLookupElem( @@ -289,7 +286,7 @@ impl BpfObject { ))); } - Ok(value) + audit_value_entry.to_audit_entry() } /** @@ -396,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)); } @@ -410,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 new file mode 100644 index 00000000..667cd36f --- /dev/null +++ b/shared-ebpf/include/gpa_audit_event.h @@ -0,0 +1,85 @@ +// 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_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])"); 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