From a092e09bcdf25f299e82ec25f4dd553d41928199 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Thu, 20 Aug 2026 21:25:23 +0000 Subject: [PATCH 1/2] localIPBindMonitorOnly --- ebpf/redirect.bpf.c | 58 +++++-- linux-ebpf/ebpf_cgroup.c | 65 ++++++-- proxy_agent/config/GuestProxyAgent.linux.json | 1 + .../config/GuestProxyAgent.windows.json | 1 + proxy_agent/src/common/config.rs | 18 ++- proxy_agent/src/redirector.rs | 141 +++++++++++++----- proxy_agent/src/redirector/linux.rs | 76 ++++++++++ proxy_agent/src/redirector/shared_ebpf.rs | 11 +- proxy_agent/src/redirector/windows/bpf_api.rs | 13 ++ .../src/redirector/windows/bpf_prog.rs | 74 +++++++++ shared-ebpf/include/gpa_audit_event.h | 14 +- 11 files changed, 399 insertions(+), 73 deletions(-) diff --git a/ebpf/redirect.bpf.c b/ebpf/redirect.bpf.c index 1acee7c7..81a151e7 100644 --- a/ebpf/redirect.bpf.c +++ b/ebpf/redirect.bpf.c @@ -12,6 +12,13 @@ struct bpf_map_def policy_map = { .value_size = sizeof(destination_entry_t), .max_entries = 10}; +#pragma clang section data = "maps" +struct bpf_map_def config_map = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(uint32_t), + .value_size = sizeof(struct gpa_config_entry), + .max_entries = 1}; + #pragma clang section data = "maps" struct bpf_map_def skip_process_map = { .type = BPF_MAP_TYPE_HASH, @@ -26,6 +33,13 @@ struct bpf_map_def audit_map = { .value_size = sizeof(sock_addr_audit_entry_t), .max_entries = 1000}; +#pragma clang section data = "maps" +struct bpf_map_def audit_only_map = { + .type = BPF_MAP_TYPE_LRU_HASH, + .key_size = sizeof(sock_addr_audit_key_t), + .value_size = sizeof(sock_addr_audit_entry_t), + .max_entries = 1000}; + /* check the current pid in the skip_process map. return 1 if found, otherwise return 0. @@ -41,13 +55,21 @@ check_skip_process_map_entry(uint32_t pid) return (skip_entry != NULL) ? 1 : 0; } +inline __attribute__((always_inline)) int +local_ip_bind_monitor_only_enabled(void) +{ + uint32_t key = GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY; + struct gpa_config_entry *entry = bpf_map_lookup_elem(&config_map, &key); + return entry != NULL && entry->enabled != 0; +} + /* update audit map entry if not skip redirecting. return 0 if the entry is updated, otherwise return 1 if pid found in the skip_process_map. */ inline __attribute__((always_inline)) int -update_audit_map_entry(bpf_sock_addr_t *ctx) +update_audit_map_entry(bpf_sock_addr_t *ctx, int audit_only) { uint64_t pid_tip = bpf_get_current_pid_tgid(); uint32_t pid = (uint32_t)(pid_tip >> 32); @@ -77,6 +99,19 @@ update_audit_map_entry(bpf_sock_addr_t *ctx) entry.destination_ipv4 = ctx->user_ip4; // we only support ipv4 so far. entry.destination_port = ctx->user_port; uint16_t source_port = ctx->msg_src_port; + if (audit_only) + { + sock_addr_audit_key_t key = {0}; + key.protocol = ctx->protocol; + key.source_port = source_port != 0 ? source_port : pid; + uint64_t ret = bpf_map_update_elem(&audit_only_map, &key, &entry, 0); + if (ret != 0) + { + bpf_printk("Failed to update audit-only map with results: %u.", ret); + } + return 0; + } + if (source_port == 0) { int32_t result = bpf_sock_addr_set_redirect_context(ctx, &entry, sizeof(sock_addr_audit_entry_t)); @@ -121,23 +156,22 @@ authorize_v4(bpf_sock_addr_t *ctx) { bpf_printk("Found v4 proxy entry value: %u, %u", policy->destination_ip.ipv4, policy->destination_port); + uint32_t source_ip = ctx->msg_src_ip4; + int audit_only = local_ip_bind_monitor_only_enabled() && // check the config map for localIPBindMonitorOnly + source_ip != 0 && (source_ip & 0xff) != 0x7f; // check if the source ip is set and not loopback + // update to the audit map before changing the destination ip and port. - if (update_audit_map_entry(ctx) == 1) + if (update_audit_map_entry(ctx, audit_only) == 1) { bpf_printk("Found skip process entry, skip the redirection."); return BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT; } - // if (ctx->msg_src_ip4 == 0) - // { - // bpf_printk("Local/source ip is not set, redirect to loopback ip."); - // ctx->user_ip4 = policy->destination_ip.ipv4; - // } - // else - // { - // ctx->user_ip4 = ctx->msg_src_ip4; - // bpf_printk("Local/source ip is set, redirect to source ip:%u.", ctx->user_ip4); - // } + if (audit_only) + { + bpf_printk("Source address is explicitly bound, audit without redirecting."); + return BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT; + } bpf_printk("redirecting to destination loopback ip."); ctx->user_ip4 = policy->destination_ip.ipv4; diff --git a/linux-ebpf/ebpf_cgroup.c b/linux-ebpf/ebpf_cgroup.c index a58f82f7..98136bbc 100644 --- a/linux-ebpf/ebpf_cgroup.c +++ b/linux-ebpf/ebpf_cgroup.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "socket.h" @@ -27,6 +28,13 @@ struct { __uint(max_entries, 10); } policy_map SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); + __type(value, struct gpa_config_entry); + __uint(max_entries, 1); +} config_map SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __type(key, struct gpa_audit_key); // source port and protocol @@ -34,6 +42,13 @@ struct { __uint(max_entries, 200); // LRU evicts oldest on overflow } audit_map SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __type(key, struct gpa_audit_key); + __type(value, struct gpa_audit_event); + __uint(max_entries, 200); +} audit_only_map SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __type(key, __u64); // pid-tgid or socket cookie @@ -57,13 +72,21 @@ check_skip_process_map_entry(__u32 pid) return (skip_entry != NULL) ? 1 : 0; } +static __always_inline int +local_ip_bind_monitor_only_enabled(void) +{ + __u32 key = GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY; + struct gpa_config_entry *entry = bpf_map_lookup_elem(&config_map, &key); + return entry != NULL && entry->enabled != 0; +} + /* update audit map entry if not skip redirecting. return 0 if the entry is updated, otherwise return 1 if pid found in the skip_process_map. */ static __always_inline int -update_local_map_entry(struct bpf_sock_addr *ctx) +update_local_map_entry(struct bpf_sock_addr *ctx, __u32 audit_only) { __u64 pid_tip = bpf_get_current_pid_tgid(); __u32 pid = (__u32)(pid_tip >> 32); @@ -81,6 +104,7 @@ update_local_map_entry(struct bpf_sock_addr *ctx) entry.destination_ipv4 = ctx->user_ip4; // we only support ipv4 so far. entry.destination_port = ctx->user_port; entry.protocol = ctx->protocol; + entry.audit_only = audit_only; __u64 ret = bpf_map_update_elem(&local_map, &pid_tip, &entry, 0); if (ret != 0) @@ -109,27 +133,30 @@ authorize_v4(struct bpf_sock_addr *ctx) { bpf_printk("authorize_v4: Found v4 proxy entry value: %u, %u", policy->destination_ip.ipv4, policy->destination_port); + // At connect4, msg_src_ip4 is not valid; it is only populated for + // UDP sendmsg hooks. A concrete address set by bind(2) is available + // from the socket before TCP performs automatic source selection. + __u32 source_ip = ctx->sk != NULL ? ctx->sk->src_ip4 : 0; + __u32 source_ip_host = bpf_ntohl(source_ip); + __u32 audit_only = local_ip_bind_monitor_only_enabled() && + source_ip != 0 && + (source_ip_host & 0xff000000) != 0x7f000000; + // update to the audit map before changing the destination ip and port. - if (update_local_map_entry(ctx) == 1) + if (update_local_map_entry(ctx, audit_only) == 1) { bpf_printk("authorize_v4: Found skip process entry, skip the redirection."); return BPF_SOCK_ADDR_VERDICT_PROCEED; } - // TODO: check if the local ip is set. - // __u32 local_ip; - // __u64 read = bpf_probe_read_kernel(&local_ip, sizeof(__u32), &ctx->msg_src_ip4); - // if (read == 0 && local_ip != 0) - // { - // // read the local ip from the msg_src_ip4 successfully and ip is set. - // ctx->user_ip4 = local_ip; - // bpf_printk("authorize_v4: Local/source ip is set, redirect to source ip:%u.", local_ip); - // } - // else + if (audit_only) { - ctx->user_ip4 = policy->destination_ip.ipv4; - bpf_printk("authorize_v4: Local/source ip is not set, redirect to loopback ip."); + bpf_printk("authorize_v4: Source address is explicitly bound, audit without redirecting."); + return BPF_SOCK_ADDR_VERDICT_PROCEED; } + + ctx->user_ip4 = policy->destination_ip.ipv4; + bpf_printk("authorize_v4: Local/source ip is not set, redirect to loopback ip."); ctx->user_port = policy->destination_port; } @@ -157,7 +184,15 @@ update_audit_map_entry_sk(__u32 local_port, struct gpa_sock_addr_local_entry *lo entry.destination_ipv4 = local_entry->destination_ipv4; entry.destination_port = local_entry->destination_port; - __u64 ret = bpf_map_update_elem(&audit_map, &key, &entry, 0); + __u64 ret; + if (local_entry->audit_only) + { + ret = bpf_map_update_elem(&audit_only_map, &key, &entry, 0); + } + else + { + ret = bpf_map_update_elem(&audit_map, &key, &entry, 0); + } if (ret != 0) { bpf_printk("update_audit_map_entry_sk: Failed to update audit map entry with results:%u.", ret); diff --git a/proxy_agent/config/GuestProxyAgent.linux.json b/proxy_agent/config/GuestProxyAgent.linux.json index 90b6d0b5..a4693012 100644 --- a/proxy_agent/config/GuestProxyAgent.linux.json +++ b/proxy_agent/config/GuestProxyAgent.linux.json @@ -10,5 +10,6 @@ "fileLogLevel": "Trace", "fileLogLevelForEvents": "Info", "fileLogLevelForSystemEvents": "Info", + "localIPBindMonitorOnly": true, "canonicalRequestMode": "Shadow" } \ No newline at end of file diff --git a/proxy_agent/config/GuestProxyAgent.windows.json b/proxy_agent/config/GuestProxyAgent.windows.json index b43e5768..ba8960e8 100644 --- a/proxy_agent/config/GuestProxyAgent.windows.json +++ b/proxy_agent/config/GuestProxyAgent.windows.json @@ -9,5 +9,6 @@ "fileLogLevel": "Trace", "fileLogLevelForEvents": "Info", "fileLogLevelForSystemEvents": "Info", + "localIPBindMonitorOnly": true, "canonicalRequestMode": "Shadow" } \ No newline at end of file diff --git a/proxy_agent/src/common/config.rs b/proxy_agent/src/common/config.rs index 4365f148..2f1e2efa 100644 --- a/proxy_agent/src/common/config.rs +++ b/proxy_agent/src/common/config.rs @@ -79,6 +79,10 @@ pub fn get_enable_http_proxy_trace() -> bool { SYSTEM_CONFIG.enableHttpProxyTrace.unwrap_or(false) } +pub fn get_local_ip_bind_monitor_only() -> bool { + SYSTEM_CONFIG.get_local_ip_bind_monitor_only() +} + /// Rollout flag for the Innovation 2.1 canonical request pipeline. /// /// Read from the optional `canonicalRequestMode` key in the GPA config @@ -115,6 +119,8 @@ pub struct Config { /// This is an optional config, mainly for manual debugging purpose #[serde(skip_serializing_if = "Option::is_none")] enableHttpProxyTrace: Option, + #[serde(skip_serializing_if = "Option::is_none")] + localIPBindMonitorOnly: Option, /// Innovation 2.1 canonical request rollout flag. /// Optional; absent or unparseable values resolve to /// [`crate::proxy::canonical::CanonicalMode::Off`] so production @@ -230,6 +236,10 @@ impl Config { None } + pub fn get_local_ip_bind_monitor_only(&self) -> bool { + self.localIPBindMonitorOnly.unwrap_or(false) + } + /// Resolve the canonical-request rollout flag. /// /// Returns [`crate::proxy::canonical::CanonicalMode::Off`] when the @@ -278,7 +288,7 @@ mod tests { Err(err) => panic!("Failed to create folder: {}", err), } let config_file_path = temp_test_path.join("test_config.json"); - let config = create_config_file(config_file_path); + let mut config = create_config_file(config_file_path); assert_eq!( r#"C:\logFolderName"#, @@ -331,6 +341,10 @@ mod tests { ); } + assert!(config.get_local_ip_bind_monitor_only()); + config.localIPBindMonitorOnly = None; + assert!(!config.get_local_ip_bind_monitor_only()); + assert_eq!( proxy_agent_shared::logger::LoggerLevel::Info, config.get_file_log_level_for_events().unwrap(), @@ -364,6 +378,7 @@ mod tests { "hostGAPluginSupport": 1, "imdsSupport": 1, "ebpfProgramName": "ebpfProgramName", + "localIPBindMonitorOnly": true, "fileLogLevelForEvents": "Info", "fileLogLevelForSystemEvents": "Info" }"# @@ -378,6 +393,7 @@ mod tests { "hostGAPluginSupport": 1, "imdsSupport": 1, "ebpfProgramName": "ebpfProgramName", + "localIPBindMonitorOnly": true, "fileLogLevelForEvents": "Info", "fileLogLevelForSystemEvents": "Info" }"# diff --git a/proxy_agent/src/redirector.rs b/proxy_agent/src/redirector.rs index e94f1ee1..e0c1f86c 100644 --- a/proxy_agent/src/redirector.rs +++ b/proxy_agent/src/redirector.rs @@ -53,15 +53,12 @@ use crate::common::helpers; use crate::common::result::Result; use crate::common::{config, logger}; use crate::provision; -use crate::shared_state::access_control_wrapper::AccessControlSharedState; +use crate::proxy::Claims; use crate::shared_state::agent_status_wrapper::{AgentStatusModule, AgentStatusSharedState}; -use crate::shared_state::connection_summary_wrapper::ConnectionSummarySharedState; -use crate::shared_state::key_keeper_wrapper::KeyKeeperSharedState; -use crate::shared_state::provision_wrapper::ProvisionSharedState; +use crate::shared_state::proxy_server_wrapper::ProxyServerSharedState; use crate::shared_state::redirector_wrapper::RedirectorSharedState; use crate::shared_state::EventThreadsSharedState; use crate::shared_state::SharedState; -use proxy_agent_shared::common_state::CommonState; use proxy_agent_shared::logger::LoggerLevel; use proxy_agent_shared::misc_helpers; use proxy_agent_shared::proxy_agent_aggregate_status::ModuleState; @@ -109,28 +106,14 @@ impl AuditEntry { pub struct Redirector { local_port: u16, - redirector_shared_state: RedirectorSharedState, - key_keeper_shared_state: KeyKeeperSharedState, - agent_status_shared_state: AgentStatusSharedState, - cancellation_token: CancellationToken, - common_state: CommonState, - provision_shared_state: ProvisionSharedState, - access_control_shared_state: AccessControlSharedState, - connection_summary_shared_state: ConnectionSummarySharedState, + shared_state: SharedState, } impl Redirector { pub fn new(local_port: u16, shared_state: &SharedState) -> Self { Redirector { local_port, - cancellation_token: shared_state.get_cancellation_token(), - key_keeper_shared_state: shared_state.get_key_keeper_shared_state(), - common_state: shared_state.get_common_state(), - provision_shared_state: shared_state.get_provision_shared_state(), - agent_status_shared_state: shared_state.get_agent_status_shared_state(), - redirector_shared_state: shared_state.get_redirector_shared_state(), - access_control_shared_state: shared_state.get_access_control_shared_state(), - connection_summary_shared_state: shared_state.get_connection_summary_shared_state(), + shared_state: shared_state.clone(), } } @@ -140,7 +123,8 @@ impl Redirector { pub async fn start(&self) { let message = "eBPF redirector is starting"; if let Err(e) = self - .agent_status_shared_state + .shared_state + .get_agent_status_shared_state() .set_module_status_message(message.to_string(), AgentStatusModule::Redirector) .await { @@ -193,6 +177,13 @@ impl Redirector { logger::write_information(format!( "Success updated bpf skip_process map with pid={pid}." )); + let monitor_only = config::get_local_ip_bind_monitor_only(); + if monitor_only { + bpf_object.update_local_ip_bind_monitor_only(true)?; + } + logger::write_information(format!( + "Updated eBPF localIPBindMonitorOnly={monitor_only}." + )); // Do not update redirect policy map here, it will be updated by provision module // When provision is finished, it will call update_xxx_redirect_policy functions to update the redirect policy maps. @@ -202,14 +193,23 @@ impl Redirector { logger::write_information("Success attached bpf prog.".to_string()); if let Err(e) = self - .redirector_shared_state + .shared_state + .get_redirector_shared_state() .update_bpf_object(Arc::new(Mutex::new(bpf_object))) .await { logger::write_error(format!("Failed to update bpf object in shared state: {e}")); } + if monitor_only { + tokio::spawn(poll_audit_only( + self.shared_state.get_redirector_shared_state(), + self.shared_state.get_proxy_server_shared_state(), + self.shared_state.get_cancellation_token(), + )); + } if let Err(e) = self - .redirector_shared_state + .shared_state + .get_redirector_shared_state() .set_local_port(self.local_port) .await { @@ -222,7 +222,8 @@ impl Redirector { logger::AGENT_LOGGER_KEY, ); if let Err(e) = self - .agent_status_shared_state + .shared_state + .get_agent_status_shared_state() .set_module_status_message(message.to_string(), AgentStatusModule::Redirector) .await { @@ -231,7 +232,8 @@ impl Redirector { )); } if let Err(e) = self - .agent_status_shared_state + .shared_state + .get_agent_status_shared_state() .set_module_state(ModuleState::RUNNING, AgentStatusModule::Redirector) .await { @@ -239,23 +241,14 @@ impl Redirector { } // report redirector ready for provision - provision::redirector_ready(EventThreadsSharedState { - cancellation_token: self.cancellation_token.clone(), - common_state: self.common_state.clone(), - access_control_shared_state: self.access_control_shared_state.clone(), - redirector_shared_state: self.redirector_shared_state.clone(), - key_keeper_shared_state: self.key_keeper_shared_state.clone(), - provision_shared_state: self.provision_shared_state.clone(), - agent_status_shared_state: self.agent_status_shared_state.clone(), - connection_summary_shared_state: self.connection_summary_shared_state.clone(), - }) - .await; + provision::redirector_ready(EventThreadsSharedState::new(&self.shared_state)).await; Ok(()) } async fn get_status_message(&self) -> String { - self.agent_status_shared_state + self.shared_state + .get_agent_status_shared_state() .get_module_status(AgentStatusModule::Redirector) .await .message @@ -263,7 +256,8 @@ impl Redirector { async fn set_error_status(&self, message: String) { if let Err(e) = self - .agent_status_shared_state + .shared_state + .get_agent_status_shared_state() .set_module_status_message(message.to_string(), AgentStatusModule::Redirector) .await { @@ -274,6 +268,75 @@ impl Redirector { } } +async fn poll_audit_only( + redirector_shared_state: RedirectorSharedState, + proxy_server_shared_state: ProxyServerSharedState, + cancellation_token: CancellationToken, +) { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); + loop { + tokio::select! { + _ = cancellation_token.cancelled() => return, + _ = interval.tick() => { + let Some(bpf_object) = redirector_shared_state + .get_bpf_object() + .await + .ok() + .flatten() + else { + continue; + }; + let records = bpf_object.lock().unwrap().drain_audit_only(); + match records { + Ok(records) => { + for entry in records { + let destination_ip = entry.destination_ipv4_addr(); + let destination_port = entry.destination_port_in_host_byte_order(); + let message = match Claims::from_audit_entry( + &entry, + std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), // not used for audit-only, so just use unspecified + 0, // not used for audit-only, so just use 0 + proxy_server_shared_state.clone(), + ) + .await + { + Ok(claims) => format!( + "eBPF audit-only connection: userName={}, processId={}, processName={}, processFullPath={}, processCmdLine={}, runAsElevated={}, destination={}:{}", + claims.userName, + claims.processId, + claims.processName.to_string_lossy(), + claims.processFullPath.display(), + claims.processCmdLine, + claims.runAsElevated, + destination_ip, + destination_port, + ), + Err(err) => format!( + "eBPF audit-only connection: userId={}, processId={}, processDetails=unavailable ({err}), destination={}:{}", + entry.logon_id, + entry.process_id, + destination_ip, + destination_port, + ), + }; + event_logger::write_event( + LoggerLevel::Warn, + message, + "poll_audit_only", + "redirector", + logger::AGENT_LOGGER_KEY, + ); + } + } + Err(err) => logger::write_warning(format!( + "Failed to drain eBPF audit-only map: {err}" + )), + } + } + } + } +} + #[cfg(windows)] pub fn get_audit_from_stream_socket(raw_socket_id: usize) -> Result { windows::get_audit_from_redirect_context(raw_socket_id) diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index 36571e4e..2a2e8ab0 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -8,6 +8,7 @@ use crate::common::{ }; use crate::redirector::shared_ebpf::linux_types::{ destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, + GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY, }; use crate::redirector::{ip_to_string, AuditEntry}; use crate::shared_state::redirector_wrapper::RedirectorSharedState; @@ -91,6 +92,40 @@ impl BpfObject { Ok(()) } + pub fn update_local_ip_bind_monitor_only(&mut self, enabled: bool) -> Result<()> { + let config_map_name = "config_map"; + match self.0.map_mut(config_map_name) { + Some(map) => match HashMap::<&mut MapData, u32, [u32; 1]>::try_from(map) { + Ok(mut config_map) => config_map + .insert( + GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY, + [u32::from(enabled)], + 0, + ) + .map_err(|err| { + Error::Bpf(BpfErrorType::UpdateBpfMapHashMap( + config_map_name.to_string(), + "localIPBindMonitorOnly".to_string(), + err.to_string(), + )) + })?, + Err(err) => { + return Err(Error::Bpf(BpfErrorType::LoadBpfMapHashMap( + config_map_name.to_string(), + err.to_string(), + ))); + } + }, + None => { + return Err(Error::Bpf(BpfErrorType::GetBpfMap( + config_map_name.to_string(), + "Map does not exist".to_string(), + ))); + } + } + Ok(()) + } + pub fn update_policy_elem_bpf_map( &mut self, endpoint_name: &str, @@ -381,6 +416,47 @@ impl BpfObject { } Ok(()) } + + pub fn drain_audit_only(&mut self) -> Result> { + let audit_map_name = "audit_only_map"; + match self.0.map_mut(audit_map_name) { + Some(map) => { + let mut audit_map = HashMap::<&mut MapData, [u32; 2], [u32; 5]>::try_from(map) + .map_err(|err| { + Error::Bpf(BpfErrorType::LoadBpfMapHashMap( + audit_map_name.to_string(), + err.to_string(), + )) + })?; + let mut records = Vec::new(); + for item in audit_map.iter() { + let (key, value) = item.map_err(|err| { + Error::Bpf(BpfErrorType::MapLookupElem( + audit_map_name.to_string(), + err.to_string(), + )) + })?; + records.push(( + key, + sock_addr_audit_entry::from_array(value).to_audit_entry(), + )); + } + for (key, _) in &records { + audit_map.remove(key).map_err(|err| { + Error::Bpf(BpfErrorType::MapDeleteElem( + audit_map_name.to_string(), + err.to_string(), + )) + })?; + } + Ok(records.into_iter().map(|(_, entry)| entry).collect()) + } + None => Err(Error::Bpf(BpfErrorType::GetBpfMap( + audit_map_name.to_string(), + "Map does not exist".to_string(), + ))), + } + } } // Redirector implementation for Linux platform diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index 15bee538..63125dbc 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -68,6 +68,7 @@ pub type destination_entry = _destination_entry; pub const IPPROTO_TCP: u32 = 6; #[allow(dead_code)] pub const IPPROTO_UDP: u32 = 17; +pub const GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY: u32 = 0; #[repr(C)] pub struct sock_addr_skip_process_entry { @@ -90,7 +91,7 @@ impl sock_addr_skip_process_entry { } #[repr(C)] -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct sock_addr_audit_key { pub protocol: u32, pub source_port: u32, @@ -361,15 +362,17 @@ impl AuditValueEntry { #[cfg(not(windows))] pub mod linux_types { pub use super::{ - destination_entry, sock_addr_audit_entry, sock_addr_audit_key, sock_addr_skip_process_entry, + destination_entry, sock_addr_audit_entry, sock_addr_audit_key, + sock_addr_skip_process_entry, GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY, }; } #[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, + destination_entry as destination_entry_t, sock_addr_audit_entry, + sock_addr_audit_key as sock_addr_audit_key_t, sock_addr_skip_process_entry, + GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY, }; } diff --git a/proxy_agent/src/redirector/windows/bpf_api.rs b/proxy_agent/src/redirector/windows/bpf_api.rs index 55b52bcb..04578c59 100644 --- a/proxy_agent/src/redirector/windows/bpf_api.rs +++ b/proxy_agent/src/redirector/windows/bpf_api.rs @@ -184,6 +184,8 @@ type BpfMapLookupElem = unsafe extern "C" fn(map_fd: c_int, key: *const c_void, value: *mut c_void) -> c_int; type BpfMapDeleteElem = unsafe extern "C" fn(map_fd: c_int, key: *const c_void) -> c_int; +type BpfMapGetNextKey = + unsafe extern "C" fn(map_fd: c_int, key: *const c_void, next_key: *mut c_void) -> c_int; type LibBpfGetError = unsafe extern "C" fn(no_use_ptr: *const c_void) -> c_long; @@ -304,3 +306,14 @@ pub fn bpf_map_delete_elem(map_fd: c_int, key: *const c_void) -> Result { get_ebpf_api_fun(ebpf_api, "bpf_map_delete_elem\0")?; Ok(unsafe { map_delete_elem(map_fd, key) }) } + +pub fn bpf_map_get_next_key( + map_fd: c_int, + key: *const c_void, + next_key: *mut c_void, +) -> Result { + let ebpf_api = get_ebpf_api()?; + let map_get_next_key: Symbol = + get_ebpf_api_fun(ebpf_api, "bpf_map_get_next_key\0")?; + Ok(unsafe { map_get_next_key(map_fd, key, next_key) }) +} diff --git a/proxy_agent/src/redirector/windows/bpf_prog.rs b/proxy_agent/src/redirector/windows/bpf_prog.rs index 3a0fb5e9..11a8b732 100644 --- a/proxy_agent/src/redirector/windows/bpf_prog.rs +++ b/proxy_agent/src/redirector/windows/bpf_prog.rs @@ -9,6 +9,9 @@ use crate::common::{ error::{BpfErrorType, Error}, result::Result, }; +use crate::redirector::shared_ebpf::windows_types::{ + sock_addr_audit_entry, GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY, +}; use crate::redirector::AuditEntry; use proxy_agent_shared::misc_helpers; use std::ffi::c_void; @@ -334,6 +337,77 @@ impl BpfObject { Ok(()) } + pub fn update_local_ip_bind_monitor_only(&self, enabled: bool) -> Result<()> { + let map_name = "config_map"; + let map_fd = self.get_bpf_map_fd(map_name)?; + let key = GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY; + let value = [u32::from(enabled)]; + + let result = bpf_map_update_elem( + map_fd, + &key as *const u32 as *const c_void, + value.as_ptr() as *const c_void, + 0, + ) + .map_err(|e| { + Error::Bpf(BpfErrorType::UpdateBpfMapHashMap( + map_name.to_string(), + "localIPBindMonitorOnly".to_string(), + e.to_string(), + )) + })?; + if result != 0 { + return Err(Error::Bpf(BpfErrorType::UpdateBpfMapHashMap( + map_name.to_string(), + "localIPBindMonitorOnly".to_string(), + format!("bpf_map_update_elem returned error code {result}"), + ))); + } + Ok(()) + } + + pub fn drain_audit_only(&self) -> Result> { + let map_name = "audit_only_map"; + let map_fd = self.get_bpf_map_fd(map_name)?; + let mut keys = Vec::new(); + let mut previous_key: Option = None; + + loop { + let mut next_key = sock_addr_audit_key_t::from_array([0; 2]); + let previous_key_ptr = previous_key + .as_ref() + .map_or(std::ptr::null(), |key| key as *const _ as *const c_void); + let result = bpf_map_get_next_key( + map_fd, + previous_key_ptr, + &mut next_key as *mut sock_addr_audit_key_t as *mut c_void, + )?; + if result != 0 { + break; + } + previous_key = Some(next_key); + keys.push(next_key); + } + + let mut records = Vec::with_capacity(keys.len()); + for key in keys { + let mut value = sock_addr_audit_entry::empty(); + 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 as *mut c_void, + )?; + if result == 0 { + records.push(value.to_audit_entry()); + let _ = bpf_map_delete_elem( + map_fd, + &key as *const sock_addr_audit_key_t as *const c_void, + )?; + } + } + Ok(records) + } + /** Routine Description: This routine delete element from policy_map. diff --git a/shared-ebpf/include/gpa_audit_event.h b/shared-ebpf/include/gpa_audit_event.h index 667cd36f..816450a7 100644 --- a/shared-ebpf/include/gpa_audit_event.h +++ b/shared-ebpf/include/gpa_audit_event.h @@ -12,6 +12,8 @@ #pragma once +#define GPA_CONFIG_LOCAL_IP_BIND_MONITOR_ONLY 0 + // 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 @@ -63,8 +65,14 @@ struct gpa_skip_process_entry __u32 pid; }; +// Runtime configuration passed from GPA user mode to the eBPF program. +struct gpa_config_entry +{ + __u32 enabled; +}; + // Local address entry - tracks current connection state in the local_map -// Size: 24 bytes (6 x u32) +// Size: 28 bytes (7 x u32) struct gpa_sock_addr_local_entry { __u32 logon_id; // uid @@ -73,6 +81,7 @@ struct gpa_sock_addr_local_entry __u32 destination_ipv4; __u32 destination_port; __u32 protocol; + __u32 audit_only; }; // Compile-time layout assertions to guarantee binary compatibility with Rust loader. @@ -82,4 +91,5 @@ _Static_assert(sizeof(struct gpa_destination_entry) == 24, "destination_entry mu _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])"); +_Static_assert(sizeof(struct gpa_config_entry) == 4, "config_entry must be 4 bytes ([u32; 1])"); +_Static_assert(sizeof(struct gpa_sock_addr_local_entry) == 28, "sock_addr_local_entry must be 28 bytes ([u32; 7])"); From ea503eb13471d156fea576099c574d00ce767180 Mon Sep 17 00:00:00 2001 From: Zhidong Peng Date: Thu, 20 Aug 2026 21:30:11 +0000 Subject: [PATCH 2/2] fix naming issue caught by clippy::wrong-self-convention --- proxy_agent/src/redirector/linux.rs | 14 +++++++------- proxy_agent/src/redirector/shared_ebpf.rs | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/proxy_agent/src/redirector/linux.rs b/proxy_agent/src/redirector/linux.rs index 2a2e8ab0..33c3e9f5 100644 --- a/proxy_agent/src/redirector/linux.rs +++ b/proxy_agent/src/redirector/linux.rs @@ -64,7 +64,7 @@ impl BpfObject { Ok(mut skip_process_map) => { let key = sock_addr_skip_process_entry::from_pid(pid); let value = sock_addr_skip_process_entry::from_pid(pid); - match skip_process_map.insert(key.to_array(), value.to_array(), 0) { + match skip_process_map.insert(key.as_array(), value.as_array(), 0) { Ok(_) => logger::write(format!("skip_process_map updated with {pid}")), Err(err) => { return Err(Error::Bpf(BpfErrorType::UpdateBpfMapHashMap( @@ -140,7 +140,7 @@ impl BpfObject { let local_ip = super::string_to_ip(constants::PROXY_AGENT_IP); let key = destination_entry::from_ipv4(dest_ipv4, dest_port); let value = destination_entry::from_ipv4(local_ip, local_port); - match policy_map.insert(key.to_array(), value.to_array(), 0) { + match policy_map.insert(key.as_array(), value.as_array(), 0) { Ok(_) => { logger::write(format!("policy_map updated for {endpoint_name}")); } @@ -277,7 +277,7 @@ impl BpfObject { Some(map) => match HashMap::try_from(map) { Ok(audit_map) => { let key = sock_addr_audit_key::from_source_port(source_port); - match audit_map.get(&key.to_array(), 0) { + match audit_map.get(&key.as_array(), 0) { Ok(value) => { let audit_value = sock_addr_audit_entry::from_array(value); Ok(AuditEntry { @@ -319,7 +319,7 @@ impl BpfObject { Ok(mut policy_map) => { let key = destination_entry::from_ipv4(dest_ipv4, dest_port); if !redirect { - match policy_map.remove(&key.to_array()) { + match policy_map.remove(&key.as_array()) { Ok(_) => { event_logger::write_event( LoggerLevel::Info, @@ -351,7 +351,7 @@ impl BpfObject { ); let local_ip: u32 = super::string_to_ip(&local_ip); let value = destination_entry::from_ipv4(local_ip, local_port); - match policy_map.insert(key.to_array(), value.to_array(), 0) { + match policy_map.insert(key.as_array(), value.as_array(), 0) { Ok(_) => { event_logger::write_event( LoggerLevel::Info, @@ -393,7 +393,7 @@ impl BpfObject { Some(map) => match HashMap::<&mut MapData, [u32; 2], [u32; 5]>::try_from(map) { Ok(mut audit_map) => { let key = sock_addr_audit_key::from_source_port(source_port); - audit_map.remove(&key.to_array()).map_err(|err| { + audit_map.remove(&key.as_array()).map_err(|err| { Error::Bpf(BpfErrorType::MapDeleteElem( source_port.to_string(), format!("Error: {err}"), @@ -657,7 +657,7 @@ mod tests { ) .unwrap(); audit_map - .insert(key.to_array(), value.to_array(), 0) + .insert(key.as_array(), value.as_array(), 0) .unwrap(); } let audit = bpf.lookup_audit(source_port); diff --git a/proxy_agent/src/redirector/shared_ebpf.rs b/proxy_agent/src/redirector/shared_ebpf.rs index 63125dbc..d22e1c49 100644 --- a/proxy_agent/src/redirector/shared_ebpf.rs +++ b/proxy_agent/src/redirector/shared_ebpf.rs @@ -55,7 +55,7 @@ impl _destination_entry { entry } - pub fn to_array(&self) -> [u32; 6] { + pub fn as_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; @@ -85,7 +85,7 @@ impl sock_addr_skip_process_entry { entry } - pub fn to_array(&self) -> [u32; 1] { + pub fn as_array(&self) -> [u32; 1] { [self.pid] } } @@ -114,7 +114,7 @@ impl sock_addr_audit_key { } } - pub fn to_array(&self) -> [u32; 2] { + pub fn as_array(&self) -> [u32; 2] { [self.protocol, self.source_port] } @@ -157,7 +157,7 @@ impl sock_addr_audit_entry { } #[allow(dead_code)] - pub fn to_array(&self) -> [u32; 5] { + pub fn as_array(&self) -> [u32; 5] { [ self.logon_id, self.process_id, @@ -383,7 +383,7 @@ mod tests { #[test] fn destination_entry_ipv4_roundtrip_array_shape() { let entry = destination_entry::from_ipv4(0x1081_3FA8, 80); - let array = entry.to_array(); + let array = entry.as_array(); assert_eq!( array[0], 0x1081_3FA8, @@ -400,7 +400,7 @@ mod tests { #[test] fn audit_key_array_roundtrip() { let key = sock_addr_audit_key::from_source_port(1234); - let array = key.to_array(); + let array = key.as_array(); let rebuilt = sock_addr_audit_key::from_array(array); assert_eq!(rebuilt.protocol, IPPROTO_TCP, "protocol mismatch"); @@ -422,7 +422,7 @@ mod tests { let key = sock_addr_skip_process_entry::from_pid(pid); assert_eq!( - key.to_array(), + key.as_array(), [pid], "pid should roundtrip through the map key layout" ); @@ -438,7 +438,7 @@ mod tests { destination_port: 5, }; - let rebuilt = sock_addr_audit_entry::from_array(canonical.to_array()); + let rebuilt = sock_addr_audit_entry::from_array(canonical.as_array()); assert_eq!(rebuilt.logon_id, canonical.logon_id); assert_eq!(rebuilt.process_id, canonical.process_id);