Agent & Installer
Windows service agent, DeviceIoControl loop, SQLite WAL queue, and deployment
Overview
The old CLI/TUI has been replaced by two deployment-focused crates:
- exfil-agent (
agent/): A Windows service that reads packets from the kernel driver viaDeviceIoControl, queues them in SQLite WAL, and streams them to the server via gRPC - exfil-installer (
installer/): A standalone binary that writes registry configuration and creates Windows services viasc.exe
Both crates compile only on Windows. On other platforms, they print an error message and exit.
Agent (exfil-agent)
Dual-Mode Execution
The agent supports two execution modes:
pub fn run() -> Result<()> {
// Try to start as a Windows Service (SCM dispatcher)
let res = service_dispatcher::start("ExfilAgent", ffi_service_main);
if res.is_err() {
// If not launched by SCM, fall back to console/debug mode
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
return rt.block_on(run_agent());
}
Ok(())
}- Service mode: When started by the Service Control Manager (e.g.,
sc start ExfilAgent), the agent registers with SCM, reportsServiceState::Running, and handles stop signals - Console mode: When launched directly from a command prompt, the agent runs interactively for development and debugging
Startup Sequence
- Read registry configuration: Reads
HKLM\SOFTWARE\ExfilGuardian\TargetUrlto get the gRPC server address - Connect to kernel driver: Tries multiple device paths to open a handle to
\\.\ExfilGuardian:
let paths = [
c"\\\\.\\ExfilGuardian",
c"\\\\?\\ExfilGuardian",
c"\\\\.\\Global\\ExfilGuardian",
c"\\\\?\\Global\\ExfilGuardian",
c"\\\\.\\DosDevices\\ExfilGuardian",
c"\\\\?\\DosDevices\\Global\\ExfilGuardian",
];The agent retries up to 10 times with 1-second intervals in case the driver has not finished initializing.
- Initialize SQLite WAL queue: Opens (or creates) a local SQLite database at
exfil_queue.dbwith WAL journal mode for concurrent read/write - Start gRPC streaming task: Spawns a Tokio task that connects to the server and streams queued packets
- Enter IOCTL loop: Blocks on
DeviceIoControl(IOCTL_EXFIL_GET_PACKET), waiting for the kernel to deliver intercepted packets
Kernel Communication
The agent mirrors the driver's ExfilPacket struct:
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ExfilPacketKernel {
pub protocol: u8,
pub padding: [u8; 3],
pub local_ip: u32,
pub remote_ip: u32,
pub local_port: u16,
pub remote_port: u16,
pub payload_len: u32,
pub payload: [u8; 4096],
}Each DeviceIoControl call blocks until the driver completes an IRP with a packet. The returned ExfilPacketKernel is converted to a Protobuf NetworkPacket and inserted into the SQLite queue.
SQLite WAL Queue
The agent uses SQLite in WAL (Write-Ahead Logging) mode as a persistent packet queue. This ensures no data loss if the gRPC connection drops:
CREATE TABLE IF NOT EXISTS packets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data BLOB NOT NULL
);- Producer thread: The IOCTL loop serializes each
NetworkPacketto Protobuf bytes and inserts them into thepacketstable - Consumer task: A Tokio task reads up to 50 packets at a time (ordered by
id ASC), sends them through the gRPC stream, and deletes them on successful transmission - Wake mechanism: An
mpsc::channelnotifies the consumer when new packets are available, avoiding unnecessary polling
gRPC Client
The agent connects to the server using tonic:
let endpoint = tonic::transport::Channel::from_shared(target_url)?;
let channel = endpoint.connect().await?;
let mut client = ExfilServiceClient::new(channel);
client.stream_packets(ReceiverStream::new(rx_proxy)).await?;If the connection drops, the agent:
- Keeps queuing packets locally in SQLite
- Logs the disconnection
- Retries connection every 5 seconds
- On reconnect, drains the SQLite queue in order
Dependencies
| Crate | Purpose |
|---|---|
tokio | Async runtime (multi-thread) |
tonic | gRPC client |
prost | Protobuf serialization |
rusqlite | SQLite WAL queue |
windows-sys | Win32 API (CreateFileA, DeviceIoControl, GetLastError) |
windows-service | SCM integration (define_windows_service!, service_dispatcher) |
winreg | Windows Registry access |
rustls | TLS (ring crypto provider) |
Installer (exfil-installer)
The installer is a standalone binary for manual deployment (as opposed to the automated enrollment via the REST API).
Usage
exfil-installer.exe <GRPC_SERVER_URL>Example:
exfil-installer.exe http://192.168.1.50:50051What It Does
- Registry configuration: Creates
HKLM\SOFTWARE\ExfilGuardianand sets theTargetUrlvalue:
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let (key, _) = hklm.create_subkey("SOFTWARE\\ExfilGuardian")?;
key.set_value("TargetUrl", &target_url)?;- Kernel driver service: Stops and removes any existing
ExfilGuardianservice, then creates a new one:
sc create ExfilGuardian binPath= "C:\path\to\exfil_guardian.sys" type= kernel start= demand- Agent service: Stops and removes any existing
ExfilAgentservice, then creates a new one:
sc create ExfilAgent binPath= "C:\path\to\exfil-agent.exe" start= auto- Start services: Starts the kernel driver first, then the agent:
sc start ExfilGuardian
sc start ExfilAgentInstaller vs. Enrollment
| Feature | Installer (exfil-installer) | Enrollment (/api/v1/enroll) |
|---|---|---|
| Triggered by | Manual CLI execution | One-liner PowerShell from server |
| Certificate management | None (manual) | Auto-generates per-agent mTLS cert |
| Binary distribution | Expects binaries in current directory | Downloads from server /static |
| Driver installation | sc.exe create | pnputil.exe /add-driver (INF/CAT) |
| Registry protection | No driver cert setup | Imports driver cert to trusted stores |
| Use case | Development / testing | Production deployment at scale |
Logging
The agent writes log output to a file at the same path as the executable with a .log extension. This is necessary because Windows services cannot write to stdout:
macro_rules! service_log {
($($arg:tt)*) => {{
let mut exe_path = std::env::current_exe()?;
exe_path.set_extension("log");
let mut f = OpenOptions::new().create(true).append(true).open(exe_path)?;
writeln!(f, "[{}] {}", unix_timestamp, format_args!($($arg)*))?;
}};
}Log entries are timestamped with Unix seconds.