API
gRPC agent ingestion and REST dashboard endpoints
Overview
The exfil-server crate runs two concurrent servers:
| Server | Framework | Port | Purpose |
|---|---|---|---|
| gRPC | Tonic | 50051 | Agent packet ingestion via Protobuf streaming |
| REST | Axum | 8080 | Dashboard status, alerts, metrics, agent enrollment |
Both servers are started concurrently via tokio::select! and share the same ServerPipeline instance. Configuration is loaded from config.yaml at startup with sensible defaults.
Running
cargo run -p exfil-serverThe server reads config.yaml for addresses and analysis tuning. If the file is missing, all defaults are used.
gRPC Server (Tonic, port 50051)
The gRPC server handles agent-to-server communication as defined in proto/exfil.proto.
Service Definition
service ExfilService {
rpc StreamPackets(stream NetworkPacket) returns (StreamResponse);
}The StreamPackets RPC is a client-streaming call: the agent opens a persistent connection and sends a continuous stream of NetworkPacket messages. The server responds with a single StreamResponse when the stream closes.
Implementation
The gRPC receiver (server/src/grpc/receiver.rs) implements ExfilService:
- Accepts the incoming stream from the agent
- Deserializes each
NetworkPacketfrom Protobuf - Converts to analysis
PacketMetadatavia the bridge module (server/src/core/bridge.rs) - Ingests into
FlowTrackerfor flow aggregation - Expired flows are processed through the full
AnalysisPipeline(DPI + Signatures + Behavioral + Correlation) - On client disconnect, all remaining active flows are flushed and analyzed
Bridge Module
The bridge (server/src/core/bridge.rs) converts gRPC types to analysis types:
local_ip/remote_ip(u32) toIpAddrtimestamp(ms) totimestamp_ns(ns)protocol(u32) toTransportProtocol::from_ip_protocol()- DNS metadata extraction from port 53 payloads
- ICMP type/code/payload extraction
- UDP header stripping (WFP outbound includes it, inbound doesn't)
- Flow ID computation from 5-tuple hash
Server Pipeline
The ServerPipeline (server/src/core/pipeline.rs) owns:
- A
FlowTracker(from the analysis crate) wrapped inparking_lot::Mutex - The analysis
AnalysisPipeline(DPI, signatures, behavioral, correlation) - An alert channel for downstream processing
The lock is always dropped before any .await to prevent deadlocks.
TLS / mTLS Configuration
The server generates certificates at startup via rcgen if they do not already exist:
certs_rcgen/ca.pem/ca.key: Root CA (ECDSA P-256)certs_rcgen/server.pem/server.key: Server certificate (signed by CA)
mTLS is configured but currently disabled for development:
let tls = tonic::transport::ServerTlsConfig::new()
.identity(identity)
.client_ca_root(client_ca); // mTLS: verify agent certificates
let grpc_server = Server::builder()
//.tls_config(tls)? // Temporarily disabled for debugging
.add_service(ExfilServiceServer::new(grpc_service))
.serve(grpc_addr);When re-enabled, agents must present a valid client certificate signed by the server's CA to connect.
REST Server (Axum, port 8080)
The REST API serves the dashboard frontend, static files (driver binaries, agent executables), the agent enrollment workflow, and the detection pipeline's alerts and metrics.
Endpoints
The REST API drives every dashboard page — no endpoint leaves a screen empty.
Status & metrics
| Method | Path | Description |
|---|---|---|
GET | /api/status | Server health check and version |
GET | /api/v1/metrics | Pipeline metrics (packets, flows, detections, alerts) |
GET | /api/v1/metrics/history | Bounded ring of recent metric snapshots (last 120) |
Alerts
| Method | Path | Description |
|---|---|---|
GET | /api/v1/alerts | List active alerts from the correlation engine |
GET | /api/v1/alerts/{id} | Fetch a single alert |
DELETE | /api/v1/alerts/{id} | Remove an alert |
POST | /api/v1/alerts/{id}/status | Update an alert's triage status |
Flows & agents
| Method | Path | Description |
|---|---|---|
GET | /api/v1/flows | List flows currently being aggregated |
GET | /api/v1/flows/{id} | Fetch a single flow record |
GET | /api/v1/agents | List known agents (id, status, packet counters) |
Each flow carries an agent_id — a stable UUIDv5 derived from the agent's
connection IP — so the dashboard can filter flows and alerts per agent.
Signatures (YAML rules)
| Method | Path | Description |
|---|---|---|
GET | /api/v1/signatures | List loaded signature rules |
POST | /api/v1/signatures | Create or update a rule (validated, then hot-reloaded) |
POST | /api/v1/signatures/validate | Dry-run validation of a rule without writing it |
POST | /api/v1/signatures/{id}/toggle | Enable / disable a rule |
POST | /api/v1/signatures/reload | Force a reload of all rules from disk |
A notify-based filesystem watcher hot-reloads the signatures/ directory on
any change (500 ms debounce), so edits land without a server restart.
Whitelist (exclusions)
| Method | Path | Description |
|---|---|---|
GET | /api/v1/whitelist | List exclusion entries |
POST | /api/v1/whitelist | Add an exclusion |
DELETE | /api/v1/whitelist/{id} | Remove an exclusion |
Every exclusion is scoped to a protocol (dns, http, tls, icmp, tcp,
udp, or any): whitelisting a DNS resolver as dns suppresses its DNS noise
without blinding the engine to a TLS C2 channel toward the same IP.
Config & enrollment
| Method | Path | Description |
|---|---|---|
GET | /api/v1/config | Current server analysis config |
POST | /api/v1/config | Replace the in-memory analysis config |
POST | /api/v1/agent/tokens | Generate a single-use enrollment token |
GET | /api/v1/enroll/windows | Download the auto-deploy PowerShell script |
GET | /static/* | Serve static files (agent binary, driver, certs) |
Alert Response
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"category": "DnsTunneling",
"confidence": 0.82,
"severity": "High",
"summary": "DNS tunneling detected: high entropy subdomains to evil.com"
}
]Metrics Response
{
"packets_received": 142857,
"active_flows": 23,
"flows_processed": 4521,
"detections_generated": 17,
"alerts_generated": 3,
"active_alerts": 2,
"signature_rules": 12
}Status Response
{
"status": "Running",
"version": "0.3.0-analysis"
}Enrollment Flow
Agent enrollment follows a Teleport-style workflow: the admin generates a single-use token on the server, which produces a one-liner PowerShell command that fully deploys the agent on a target endpoint.
Step 1: Generate Token (Admin)
curl -X POST http://server:8080/api/v1/agent/tokensResponse:
{
"token": "a1b2c3d4-...",
"command": "Invoke-RestMethod -Uri \"http://server:8080/api/v1/enroll/windows?token=a1b2c3d4-...\" | Invoke-Expression"
}The token is a UUID v4 stored in an in-memory HashSet<String> and is consumed on first use (single-use).
Step 2: Run on Target Endpoint (One-Liner)
The admin copies the command value and runs it in an elevated PowerShell session on the target Windows machine. The returned script performs:
- Create directories:
C:\Program Files\ExfilGuardian\andcerts\ - Write mTLS certificates: The server dynamically generates a unique agent certificate signed by its CA and embeds the PEM files (base64-encoded) directly in the script
- Download binaries: Fetches
exfil-agent.exe,exfil_guardian.sys,.inf,.cat, anddriver.cerfrom/static - Install driver certificate: Imports the driver's test signing certificate into
Cert:\LocalMachine\RootandCert:\LocalMachine\TrustedPublisher - Install driver package:
pnputil.exe /add-driver exfil_guardian.inf /install - Configure registry: Sets
HKLM\SOFTWARE\ExfilGuardian\TargetUrltohttps://<server_ip>:50051 - Create Windows services:
ExfilGuardian(kernel, demand-start): the WFP driverExfilAgent(user-mode, auto-start): the gRPC agent
- Start both services: Driver first, then agent after a 2-second delay
Agent Certificate Generation
Each enrollment generates a unique client certificate:
let mut client_params = CertificateParams::default();
client_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let mut client_dn = DistinguishedName::new();
client_dn.push(DnType::CommonName, format!("exfil-agent-{}", &token[0..6]));
let client_cert = Certificate::from_params(client_params)?;
let agent_cert_pem = client_cert.serialize_pem_with_signer(&ca_cert)?;The certificate's Common Name includes the first 6 characters of the enrollment token for identification. The CA private key is loaded from certs_rcgen/ca.key to sign each agent certificate.