A lightweight containerised ICMP monitoring and detection worker that turns raw packet capture into extensible observability and actions based on a rule engine.
First class integration and examples for Prometheus and Grafana ecosystem, but uses OpenTelemetry for portability.
- .NET 8 SDK
- Docker & Docker Compose (optional, for containerized builds/runs)
git clone https://github.com/SaeedMahar2006/Modux-Project.git
cd Modux-Projectdotnet restore IcmpTunnelDetection.sln
dotnet build IcmpTunnelDetection.slndotnet run --project IcmpSniffer.Workerdocker compose build icmpsniffer
docker compose up icmpsnifferSample compose and observability configs live in Example/. The LLM demo rule is in Detection/flow-rules.json (it emits packet_sampling + llm_judge commands), and the pattern is fully extensible/customizable by adding your own actions, consumers, or judge implementations.
The system includes a powerful LLM-based packet analysis capability that allows you to leverage large language models to judge ICMP packets for suspicious activity. When enabled, the LLM judge can:
- Analyze individual ICMP packets for signs of tunneling, exfiltration, or other malicious activity
- Return structured verdicts with
suspicious(bool) andreasonfields - Process packet payloads in hex and base64 formats for comprehensive analysis
- Support custom prompts to tailor the analysis to your specific security requirements
The LLM judge works by:
- Packet Sampling: When a rule triggers (e.g.,
LLM_DEMO_SAMPLING), it enables packet sampling for the flow - LLM Policy Application: The rule also creates an LLM judge policy specifying analysis parameters
- Asynchronous Processing: The
LlmPacketJudgeConsumerprocesses sampled packets and sends them to the LLM - Verdict Collection: Results are logged with detailed information about suspicious packets
The default implementation uses OpenAI-compatible APIs (configurable via OpenAI:* settings) and can be customized by implementing the ILlmPacketJudge interface.
Here is example log it emits
icmpsniffer-worker | LLM packet verdict: suspicious=True flow=[redacted] <-> [redacted] id=8 src=[redacted] dst=[redacted] type=8 code=0 bytes=27 reason=The ICMP Echo Request packet has an unusually large and structured payload of 27 bytes. The hexadecimal payload shows a distinct pattern with an initial header-like sequence (10-01-22-0F) followed by a mix of data, including what resembles a timestamp (0E-E1-0C-3E) and trailing data. This is atypical for normal ICMP Echo traffic, which typically carries minimal or simple payloads like sequential data for reachability testing. The size and apparent structure suggest the payload could be encapsulating covert data for tunneling or exfiltration, as it aligns with known ICMP tunneling techniques.
The system features a sophisticated packet sampling architecture that enables efficient analysis of suspicious flows:
Rule Detection → PacketSamplingActionHandler → FlowTracker (Buffering)
↓
ActionSupervisorService
↓
IPacketSampleConsumer implementations
Key Components:
- PacketSamplingActionHandler: Stores sampling policies for flows when rules trigger
- FlowTracker: Buffers packet samples according to policy specifications (count/percentage + cap)
- ActionSupervisorService: Orchestrates sampling sessions and dispatches samples to consumers
- IPacketSampleConsumer: Interface for components that need to analyze sampled packets
Sampling Modes:
- Count-based: Sample a fixed number of packets
- Percentage-based: Sample a percentage of packets
- Cap: Optional hard cap on total samples per flow
By default, sampling is only enabled when a rule emits a packet_sampling action. This architecture allows multiple consumers to analyze the same packet samples, enabling parallel processing for different analysis needs.
The system uses a robust action management system that dispatches immediate actions and manages long-running or sampling actions based on rule detections:
Flow Detection → Rule Evaluation → ActionCommand Creation
↓
FlowRulesEvaluator → IActionHandler implementations
↓
ActionSupervisorService (sampling + sessions)
↓ ↓
IPacketSampleConsumer IActionSessionProvider
Action Types:
- Immediate Actions (
IActionHandler): Execute immediately when rules trigger (e.g., logging, alerts) - Session-based Actions (
IActionSessionProvider): Long-running sessions for continuous monitoring - Sampling-based Actions (
IPacketSampleConsumer): Analyze sampled packets from suspicious flows
Action Lifecycle:
- Immediate actions are dispatched by
FlowRulesEvaluatorwhen rules match - Sampling and session policies are synchronized by
ActionSupervisorService - Policies are synchronized periodically (configurable interval)
- Sessions are automatically cleaned up when flows disappear
- Idempotency keys prevent duplicate action execution
The architecture supports extensibility - new action types can be added by implementing the appropriate interfaces and will be automatically discovered and registered.
Domain/— core domain models, shared interfaces, and utilities.Detection/— your custom detection logic and handlers; definesflow-rules.jsonso you can extend rule definitions here. Review RulesEngine docs for syntax, follow anonymous object style by example given.IcmpSniffer.Worker/— worker service that does packet capture and wiring:- references
Domainand linksDetection/flow-rules.json - exports metrics
- references
docker-compose.yaml— builds/runs the worker container (host network, NET_RAW/NET_ADMIN caps) for on-host packet capture.
Settings live in IcmpSniffer.Worker/appsettings.json and can be overridden with environment variables. Docker Compose uses the same env var names under services.icmpsniffer.environment.
| Setting (appsettings.json) | Default (if unset) | What it does | Compose env var |
|---|---|---|---|
Logging:LogLevel:Default |
Information |
Default minimum log level for the app | Logging__LogLevel__Default |
Logging:LogLevel:Microsoft.Hosting.Lifetime |
Information |
Log level for host lifecycle logs (start/stop, etc.) | Logging__LogLevel__Microsoft.Hosting.Lifetime |
IcmpCapture:DeviceNames |
[] |
Interface/device names to capture on. Empty means auto-selection. | IcmpCapture__DeviceNames__0, IcmpCapture__DeviceNames__1, ... |
IcmpCapture:Filter |
icmp or icmp6 |
BPF filter applied to capture (SharpPcap/libpcap style) | IcmpCapture__Filter |
IcmpCapture:DetectionInterval |
00:00:05 |
How often to run detection over current flows | IcmpCapture__DetectionInterval |
IcmpCapture:FlowIdleTimeout |
00:05:00 |
How long a flow can be idle before it is pruned | IcmpCapture__FlowIdleTimeout |
IcmpCapture:LogPackets |
true |
Enable per-packet logging (queued off capture threads) | IcmpCapture__LogPackets |
IcmpCapture:PacketLogQueueCapacity |
2048 |
Bounded queue capacity for per-packet logs | IcmpCapture__PacketLogQueueCapacity |
FlowTracker:MaxSamplesPerFlow |
256 |
Max packet samples stored per sampling-enabled flow | FlowTracker__MaxSamplesPerFlow |
Actions:LlmPacketJudgeType |
"" |
Override ILlmPacketJudge type name (empty uses built-in default)(Only for LLM capability) | Actions__LlmPacketJudgeType |
OpenAI:ApiKey |
null |
API key for LLM judge (Only for LLM capability) | OpenAI__ApiKey |
OpenAI:DefaultModel |
deepseek/deepseek-v3.2 |
Default model id used by the LLM judge (Only for LLM capability) | OpenAI__DefaultModel |
OpenAI:BaseUrl |
https://openrouter.ai/api/v1 |
OpenAI-compatible base URL (Only for LLM capability) | OpenAI__BaseUrl |
OpenAI:Prompt |
null |
Optional custom prompt for the LLM judge (Only for LLM capability) | OpenAI__Prompt |
ActionSupervisor:PolicySyncInterval |
00:00:02 |
How often to refresh action policy | ActionSupervisor__PolicySyncInterval |
ActionSupervisor:SamplingInterval |
00:00:01 |
Flow sampling interval used by action supervisor | ActionSupervisor__SamplingInterval |
ActionSupervisor:StrictMonitoringInterval |
00:00:01 |
Strict monitoring interval used by action supervisor | ActionSupervisor__StrictMonitoringInterval |
Metrics:Enabled |
true |
Enable/disable metrics endpoint/export | Metrics__Enabled |
Metrics:Port |
9464 |
Port to expose metrics on (host network recommended) | Metrics__Port |
OpenTelemetry:Otlp:Enabled |
true |
Enable/disable OTLP exporter for logs/metrics | OpenTelemetry__Otlp__Enabled |
OpenTelemetry:Otlp:Endpoint |
null |
OTLP collector endpoint (e.g. http://localhost:4317) |
OpenTelemetry__Otlp__Endpoint |
OpenTelemetry:Otlp:Headers |
null |
OTLP headers (e.g. Authorization=Bearer <token>) |
OpenTelemetry__Otlp__Headers |
OpenTelemetry:Otlp:Protocol |
null |
grpc or http/protobuf |
OpenTelemetry__Otlp__Protocol |
OpenTelemetry:Otlp:TimeoutMilliseconds |
null |
OTLP export timeout in ms | OpenTelemetry__Otlp__TimeoutMilliseconds |
ASPNETCORE_ENVIRONMENT |
Production |
Selects appsettings.{Environment}.json overlay (e.g. Development) |
ASPNETCORE_ENVIRONMENT |
Notes:
- TimeSpan values use
hh:mm:ss(example:00:00:05). Longer formats liked.hh:mm:ssalso work. - Prefer env vars for secrets like
OpenAI__ApiKey.
Example
services:
icmpsniffer:
environment:
- ASPNETCORE_ENVIRONMENT=Development
# Logging
- Logging__LogLevel__Default=Information
- Logging__LogLevel__Microsoft.Hosting.Lifetime=Information
# Capture
- IcmpCapture__Filter=icmp or icmp6
- IcmpCapture__DetectionInterval=00:00:05
- IcmpCapture__FlowIdleTimeout=00:05:00
# Example: pick interfaces explicitly
- IcmpCapture__DeviceNames__0=eth0
- IcmpCapture__DeviceNames__1=eth1
# Flow tracking
- FlowTracker__MaxSamplesPerFlow=256
# LLM judge (optional)
- Actions__LlmPacketJudgeType=
- OpenAI__ApiKey=REPLACE_ME
- OpenAI__DefaultModel=deepseek/deepseek-v3.2
- OpenAI__BaseUrl=https://openrouter.ai/api/v1
# Metrics
- Metrics__Enabled=true
- Metrics__Port=9464
Make sure to change values and settings.
The system is designed for easy extension at multiple levels, with automatic discovery and dependency injection support.
- Custom Rules: Modify
Detection/flow-rules.jsonto add detection logic using RulesEngine syntax - Action Handlers: Implement
IActionHandlerfor immediate actions triggered by rule matches - Packet Consumers: Implement
IPacketSampleConsumerfor analyzing sampled packet data - Session Providers: Implement
IActionSessionProviderfor long-running monitoring sessions - LLM Judges: Implement
ILlmPacketJudgeto replace or extend AI-based packet analysis
- Location: Place implementation classes in the
Detectionproject - Discovery:
AddDetectionActions()scans the assembly for concrete types implementing extension interfaces - Registration: All found implementations are registered as singletons with the DI container
- Activation: Components are instantiated with full dependency injection support
IActionHandler: Handle immediate actions withTypeandHandle()IPacketSampleConsumer: Analyze packet samples withWants()andConsumeAsync()IActionSessionProvider: Manage long-running sessions withGetDesiredSessions()andRunSessionAsync()ILlmPacketJudge: Judge individual packets withJudgeAsync()
- Create Implementation: Add a class implementing the appropriate interface
- Define Policy Classes: Create record types for action configuration (serialized as camelCase JSON)
- Add to Rules: Use
ActionCommandFactory.Create()inflow-rules.jsonto trigger your extension - Test: The system automatically picks up your implementation
- Deploy: No additional configuration needed - just build and run
- Override LLM Judge: Set
Actions:LlmPacketJudgeTypeto your custom implementation - Multiple Consumers: Multiple
IPacketSampleConsumerimplementations can analyze the same packets - Session Management: Use
Pinned = truefor sessions that should survive flow pruning - Policy Evolution: Use configuration hashes to detect and reload changed policies
The detector evaluates rules from Detection/flow-rules.json using RulesEngine. Each rule runs against a single FlowFact and returns a DetectionAction with optional action commands.
- Rules live in
Detection/flow-rules.jsonand are copied to the worker output at build time. - Expressions are
LambdaExpressionand useinputas theFlowFactinstance. - Common fields:
TotalPackets,TotalBytes,AveragePayloadBytes,DurationSeconds,PacketsPerSecond,BytesPerSecond,ForwardPackets,ReversePackets,IcmpType,IcmpCode.
Minimal rule pattern (note the OutputExpression fields):
"Actions": {
"OnSuccess": {
"Name": "OutputExpression",
"Context": {
"Expression": "new(\"MY_RULE\" as Rule, TunnelSeverity.Medium as Severity, 2.0 as Score, \"My reason\" as Reason, null as Commands)"
}
}
}
Meaning of fields
Rule: rule name used for logging/aggregation.Severity:TunnelSeverityenum value (Low/Medium/High).Score: numeric weight used when aggregating multiple matches.Reason: human-readable reason stored on the detection.Commands: optional list ofActionCommandobjects to trigger actions.
Use ActionCommandFactory.Create(type, payload, idempotencyKey, pinned) in the rule expression:
ActionCommandFactory.Create("packet_sampling",
new (PacketSamplingMode.Count as mode, 5 as amount, 50 as cap),
null, false)
Notes:
typemust match anIActionHandler.Type(case-insensitive).- Payload is serialized with camelCase keys (that's what
ActionCommandFactorydoes). idempotencyKeysuppresses duplicates;pinnedscopes idempotency to the flow key (survives pruning).
Create your action class in the Detection project (same assembly) and implement one of:
IActionHandlerfor immediate rule-triggered actions.IPacketSampleConsumerif you need sampled packets.IActionSessionProviderfor long-running sessions.
Example handler skeleton:
public sealed class MyActionHandler : IActionHandler
{
public const string ActionType = "my_action";
public string Type => ActionType;
public void Handle(IcmpFlow flow, FlowFact fact, ActionCommand command)
{
var policy = command.Payload.Deserialize<MyActionPolicy>(new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
// do something with policy + flow/fact
}
}
public sealed record MyActionPolicy(string? Tag = null, int? Threshold = null);
Naming conventions
- Rule names:
UPPER_SNAKE(e.g.,HIGH_VOLUME_SHORT_WINDOW). - Action types:
lower_snake(e.g.,packet_sampling,llm_judge). - Payload fields: camelCase (recommended to match
ActionCommandFactory).
The built-in LLM path is a good example of the pattern:
- Rule emits
packet_sampling+llm_judgecommands (seeDetection/flow-rules.json). PacketSamplingActionHandlerstores sampling policies for flows.LlmPacketJudgeActionHandlerstores per-flow LLM policies.LlmPacketJudgeConsumer(anIPacketSampleConsumer) reads sampled packets and callsILlmPacketJudge.- Default judge is
OpenAiLlmPacketJudge, configured viaOpenAI:*settings. - To override the judge, set
Actions:LlmPacketJudgeTypeto a type name that implementsILlmPacketJudge(example:IcmpTunnelDetection.Detection.Actions.MyCustomJudge).
AddDetectionActions(...) auto-registers all concrete types in the Detection assembly that implement
IActionHandler, IPacketSampleConsumer, or IActionSessionProvider.
So: place your classes in the Detection project, keep them non-abstract, and they will be picked up.
| Project | Package (or group) | License | Used? | Notes |
|---|---|---|---|---|
| Domain | RulesEngine | MIT | ✅ | Rule evaluation engine (FlowRulesEvaluator) |
| Detection | OpenAI | MIT | ✅ | LLM judge client |
| Detection | Microsoft.Extensions.Logging.Abstractions | MIT | ✅ | Logging for LLM consumer |
| Detection | Microsoft.Extensions.Configuration.Abstractions | MIT | ✅ | IConfiguration in action registration |
| Detection | Microsoft.Extensions.DependencyInjection.Abstractions | MIT | ✅ | IServiceCollection in action registration |
| Worker | SharpPcap | MIT | ✅ | Live packet capture |
| Worker | PacketDotNet | MPL-2.0 | ✅ | Packet parsing |
| Worker | RulesEngine | MIT | ✅ | Workflow model load |
| Worker | OpenTelemetry stack (core + OTLP + Prometheus + runtime) | Apache-2.0 | ✅ | Metrics + OTLP export + /metrics endpoint |