ExfilGuardian
Architecture

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

IOCTLDirectionDescription
IOCTL_EXFIL_GET_PACKETDriver -> AgentRetrieves 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:

  1. The agent pre-posts an IRP_MJ_DEVICE_CONTROL request
  2. The driver pends the IRP (returns STATUS_PENDING)
  3. When a WFP callout fires, the driver copies the ExfilPacket into the IRP output buffer and completes the IRP
  4. 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:

MechanismAPIPurpose
Anti-killObRegisterCallbacksRegisters object callbacks to strip PROCESS_TERMINATE access from handles to the agent process. Prevents taskkill / process termination.
Registry protectionCmRegisterCallbackExIntercepts registry operations on the driver's service key. Blocks deletion or modification of startup configuration.
DKOM bypassDirect kernel object manipulationHides 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

LayerTechnology
TransportTCP with HTTP/2 framing
AuthenticationmTLS (mutual TLS with X.509 certificates)
SerializationProtocol Buffers v3
RPC FrameworkTonic (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 CA
  • server.crt / server.key: Server identity
  • agent.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

MethodPathDescription
GET/api/alertsList alerts with filtering and pagination
GET/api/alerts/:idGet alert details with associated detections
GET/api/metricsCurrent pipeline statistics
POST/api/enrollAgent 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 APIs
  • nodeIntegration: false: no require() 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

On this page