Inter-Process Communication
How ExfilGuardian components communicate
ExfilGuardian has three IPC boundaries, each using a different protocol optimized for its constraints.
Boundary 1: Driver to Agent (DeviceIoControl)
The kernel driver and the userspace agent communicate via Windows device I/O control. The agent opens a handle to the driver's device object (\\.\ExfilGuardian) and issues DeviceIoControl calls.
IOCTL Codes
| IOCTL | Direction | Description |
|---|---|---|
IOCTL_EXFIL_GET_PACKET | Driver -> Agent | Retrieves the next intercepted packet from the kernel buffer |
Inverted IRP Model
The driver uses an inverted IRP (I/O Request Packet) pattern for efficient delivery:
- The agent pre-posts an
IRP_MJ_DEVICE_CONTROLrequest - The driver pends the IRP (returns
STATUS_PENDING) - When a WFP callout fires, the driver copies the
ExfilPacketinto the IRP output buffer and completes the IRP - The agent processes the packet and immediately posts a new IRP
This avoids polling and provides event-driven, low-latency packet delivery from kernel to userspace.
ExfilPacket Struct
The data transferred across the kernel/user boundary:
#[repr(C)]
struct ExfilPacket {
protocol: u8, // IANA protocol number (6=TCP, 17=UDP, 1=ICMP)
direction: u8, // 0 unknown, 1 outbound, 2 inbound
ip_version: u8, // 4 or 6
padding: u8,
pid: u32, // owning process PID (from WFP metadata)
local_ip: [u8; 16], // source IP (IPv4 in first 4 bytes, or full IPv6)
remote_ip: [u8; 16], // destination IP
local_port: u16, // source port
remote_port: u16, // destination port
payload_len: u32, // captured payload length (<= 4096)
wire_size: u32, // real datagram/chunk byte count (NOT capped at 4096)
payload: [u8; 4096], // raw payload (4 KiB capture buffer)
}
// No kernel timestamp: it is stamped agent-side at processing time.Driver Self-Protection
The driver protects itself and the agent process against tampering:
| Mechanism | API | Purpose |
|---|---|---|
| Anti-kill | ObRegisterCallbacks | Registers object callbacks to strip PROCESS_TERMINATE access from handles to the agent process. Prevents taskkill / process termination. |
| Registry protection | CmRegisterCallbackEx | Intercepts registry operations on the driver's service key. Blocks deletion or modification of startup configuration. |
| DKOM bypass | Direct kernel object manipulation | Hides the driver from the loaded module list to resist enumeration by attacker tools. |
Key files: driver/src/security/process.rs, driver/src/security/registry.rs, driver/src/security/filter.rs
Boundary 2: Agent to Server (gRPC Streaming)
The agent connects to the server's gRPC endpoint (server:50051) and opens a persistent streaming RPC to send intercepted packets.
Protocol Stack
| Layer | Technology |
|---|---|
| Transport | TCP with HTTP/2 framing |
| Authentication | mTLS (mutual TLS with X.509 certificates) |
| Serialization | Protocol Buffers v3 |
| RPC Framework | Tonic (Rust gRPC implementation) |
Protobuf Schema
service ExfilService {
rpc StreamPackets(stream NetworkPacket) returns (StreamResponse);
}
message NetworkPacket {
uint32 protocol = 1; // IANA protocol number
string local_ip = 2; // source IP (text form, IPv4 or IPv6)
string remote_ip = 3; // destination IP (text form)
uint32 local_port = 4; // source port
uint32 remote_port = 5; // destination port
uint64 timestamp = 6; // UNIX timestamp (ms), stamped agent-side
bytes payload = 7; // raw application-layer payload (<= 4096 bytes)
string auth_token = 8; // agent authentication token
uint32 direction = 9; // 0 unknown, 1 outbound, 2 inbound
string process = 10; // process name (filled server-side from ProcessEvent)
uint32 pid = 11; // owning process PID (from the kernel)
uint32 wire_size = 12; // real byte volume (untruncated)
string machine_id = 13; // stable machine id (set at enrollment)
}
message StreamResponse {
bool success = 1;
string message = 2;
}mTLS Authentication
Both the agent and server present certificates signed by the ExfilGuardian CA. The certs/ crate generates all certificates using rcgen:
The certificate generator (certs/src/main.rs) produces:
ca.crt/ca.key: Root CAserver.crt/server.key: Server identityagent.crt/agent.key: Agent identity
Resilience
The agent uses a SQLite WAL queue as the single FIFO buffer between packet capture and gRPC transmission. This provides:
- Durability across restarts/outages: packets persist on disk and survive an agent restart or an unreachable server
- Backpressure absorption: if the server is slow or unreachable, packets queue locally
- Ordered drain: the queue is the only emitter, so packets leave in capture order (FIFO by id)
Delivery is best-effort (at-most-once): a row is deleted once handed to the gRPC stream, without a per-message server acknowledgement — so a link break at the exact instant of draining can drop the in-flight rows. When the connection drops, the agent retries with backoff and resumes draining in FIFO order. (An at-least-once mode with a per-batch server ACK is a possible future hardening.)
Boundary 3: Server to Desktop (REST API)
The server exposes an HTTP API via Axum on port 8080. The Electron desktop app consumes this API to display alerts, metrics, and system status.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/alerts | List alerts with filtering and pagination |
GET | /api/alerts/:id | Get alert details with associated detections |
GET | /api/metrics | Current pipeline statistics |
POST | /api/enroll | Agent enrollment (certificate exchange) |
Electron IPC
The desktop app uses Electron's context-isolated IPC to bridge the React renderer and the Node.js main process, which calls the server API:
Security Model
The Electron app enforces strict isolation:
contextIsolation: true: renderer has no access to Node.js APIsnodeIntegration: false: norequire()in the renderer process- All data flows through
contextBridge; only whitelisted methods are exposed - The main process validates all data from the renderer before forwarding to the API