Implemented checker as described in internal-docs/notes4coding/checker_design.md
This commit is contained in:
188
docs/api.md
Normal file
188
docs/api.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# API Documentation
|
||||
|
||||
This document describes the input/output formats and the checker program API.
|
||||
|
||||
## CLI API
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
checker --pcap <trace.pcapng> --meta <trace.meta.jsonl> --config <modbus.json> \
|
||||
--report <report.json> [--port 502] [--mode mvp|strict] [--fail-fast]
|
||||
```
|
||||
|
||||
Exit behavior:
|
||||
|
||||
- Returns a non-zero exit code only on process-level errors (I/O, parse failures).
|
||||
- Validation findings are written to the report file.
|
||||
|
||||
## JSONL Sidecar (`trace.meta.jsonl`)
|
||||
|
||||
Each line corresponds to one packet, in the same order as the PCAP.
|
||||
|
||||
```json
|
||||
{
|
||||
"trace_id": "c7f1...",
|
||||
"event_id": 42,
|
||||
"pcap_index": 42,
|
||||
"ts_ns": 1736451234567890123,
|
||||
"direction": "c2s",
|
||||
"flow": {
|
||||
"src_ip": "10.0.0.10",
|
||||
"src_port": 51012,
|
||||
"dst_ip": "10.0.0.20",
|
||||
"dst_port": 502
|
||||
},
|
||||
"expected": {
|
||||
"modbus": {
|
||||
"transaction_id": 513,
|
||||
"unit_id": 1,
|
||||
"function_code": 3
|
||||
},
|
||||
"fields": {
|
||||
"starting_address": 0,
|
||||
"quantity": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example: `docs/examples/trace.meta.jsonl`
|
||||
|
||||
Fields:
|
||||
|
||||
- `trace_id` (string, optional): Trace identifier.
|
||||
- `event_id` (integer, optional): Event identifier from generator.
|
||||
- `pcap_index` (integer, optional): Packet index for reference.
|
||||
- `ts_ns` (integer, optional): Timestamp in nanoseconds.
|
||||
- `direction` (string, required): `c2s` (request) or `s2c` (response).
|
||||
- `flow` (object, required): Flow metadata used for request/response tracking.
|
||||
- `expected` (object, optional): Expected Modbus header and/or field values.
|
||||
|
||||
`expected.modbus`:
|
||||
|
||||
- `transaction_id` (u16, optional)
|
||||
- `unit_id` (u8, optional)
|
||||
- `function_code` (u8, optional)
|
||||
|
||||
`expected.fields`:
|
||||
|
||||
- Arbitrary JSON object whose keys match descriptor field names.
|
||||
- Values are compared against parsed output.
|
||||
|
||||
## Modbus Descriptor JSON (`modbus.json`)
|
||||
|
||||
Top-level:
|
||||
|
||||
```json
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
"function": 3,
|
||||
"name": "read_holding_registers",
|
||||
"request": [
|
||||
{"name":"starting_address","type":"u16"},
|
||||
{"name":"quantity","type":"u16"}
|
||||
],
|
||||
"response": [
|
||||
{"name":"byte_count","type":"u8"},
|
||||
{"name":"registers","type":"bytes","length_from":"byte_count"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Example: `docs/examples/modbus.json`
|
||||
|
||||
Function descriptor:
|
||||
|
||||
- `function` (u8, required): Function code.
|
||||
- `name` (string, optional): Human-readable function name.
|
||||
- `request` (array, optional): Field list for client-to-server PDUs.
|
||||
- `response` (array, optional): Field list for server-to-client PDUs.
|
||||
|
||||
Field descriptor:
|
||||
|
||||
- `name` (string, required): Field name used in output JSON.
|
||||
- `type` (string, required): `u8`, `u16`, `u32`, `i16`, `i32`, `bytes`.
|
||||
- `length` (integer, optional): Fixed length for `bytes`.
|
||||
- `length_from` (string, optional): Name of a previous numeric field.
|
||||
- `scale` (number, optional): Multiply numeric values by this scale.
|
||||
- `enum_map` (object, optional): Map numeric strings to JSON values.
|
||||
|
||||
Notes:
|
||||
|
||||
- `length_from` uses values parsed earlier in the same descriptor.
|
||||
- `bytes` output is an array of integers.
|
||||
|
||||
## Report JSON (`report.json`)
|
||||
|
||||
Structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total_packets": 1000,
|
||||
"total_findings": 8,
|
||||
"fatal": 1,
|
||||
"error": 4,
|
||||
"warn": 3,
|
||||
"info": 0
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"pcap_index": 7,
|
||||
"event_id": 42,
|
||||
"severity": "error",
|
||||
"code": "mbap_protocol",
|
||||
"message": "Protocol id is 1, expected 0",
|
||||
"flow": {
|
||||
"src_ip": "10.0.0.10",
|
||||
"src_port": 51012,
|
||||
"dst_ip": "10.0.0.20",
|
||||
"dst_port": 502
|
||||
},
|
||||
"observed": {"payload_len": 42, "mbap_length": 10},
|
||||
"expected": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Example: `docs/examples/report.json`
|
||||
|
||||
Summary fields:
|
||||
|
||||
- `total_packets`: Total packets processed.
|
||||
- `total_findings`: Total findings emitted.
|
||||
- `fatal`, `error`, `warn`, `info`: Counts by severity.
|
||||
|
||||
Finding fields:
|
||||
|
||||
- `pcap_index` (u64): Index in PCAP stream.
|
||||
- `event_id` (u64, optional): Event identifier from metadata.
|
||||
- `severity` (string): `fatal`, `error`, `warn`, `info`.
|
||||
- `code` (string): Short machine-friendly code.
|
||||
- `message` (string): Human-readable description.
|
||||
- `flow` (object, optional): Source/destination addresses.
|
||||
- `observed` (JSON, optional): Observed values for comparison.
|
||||
- `expected` (JSON, optional): Expected values for comparison.
|
||||
|
||||
## Validation Modes
|
||||
|
||||
- `mvp`: Core checks only.
|
||||
- `strict`: Treat MBAP length mismatch as a stopping condition.
|
||||
|
||||
## Internal Module API (Rust)
|
||||
|
||||
Primary modules:
|
||||
|
||||
- `config`: Descriptor types and loader for JSON config.
|
||||
- `meta`: JSONL metadata structs.
|
||||
- `decode`: PCAP packet decode to TCP payload.
|
||||
- `mbap`: MBAP parsing utilities.
|
||||
- `modbus_desc`: Descriptor-based field parsing.
|
||||
- `state`: Outstanding request tracking.
|
||||
- `validate`: End-to-end validation pipeline.
|
||||
- `report`: Report data structures.
|
||||
357
docs/design.md
Normal file
357
docs/design.md
Normal file
@@ -0,0 +1,357 @@
|
||||
## Conversation summary (so far)
|
||||
|
||||
* You’re building a **Modbus/TCP traffic generation system for security use** with this pipeline:
|
||||
**feature extraction → hybrid diffusion generates features → generator turns features into raw packets → checker validates traffic**.
|
||||
* I proposed a checker that validates traffic at **multiple layers**:
|
||||
|
||||
1. frame/IP/TCP parseability,
|
||||
2. optional TCP conversation sanity,
|
||||
3. Modbus/TCP structural + semantic validity (MBAP + PDU),
|
||||
4. optional “feature-fidelity” checks against generator sidecar metadata.
|
||||
* I recommended a **generator → checker contract** that is easy to debug and automate:
|
||||
**PCAP/PCAPNG + JSONL sidecar** (1 metadata line per packet/event).
|
||||
* You then shared your existing **descriptor-driven Modbus parser** (Rust) and a short doc.
|
||||
The public API/type model is: `Config → FunctionDescriptor → FieldDescriptor(FieldType, length/length_from/scale/enum_map)`
|
||||
and `parse_sawp_message(...)` returns JSON containing `unit`, `function`, optional `exception`, and `fields` .
|
||||
* I said: the **descriptor engine is very reusable** for the checker, but the checker’s entry point should ideally work on **raw Modbus/TCP bytes** (MBAP+PDU), not require `sawp_modbus::Message`.
|
||||
Also: for checker ergonomics, prefer structured error types over `Result<Value, String>` (but you can keep JSON output for the MVP).
|
||||
|
||||
---
|
||||
|
||||
# Implementation document: Modbus/TCP Checker (Rust)
|
||||
|
||||
This document is a practical, “not too complex” plan to implement the checker while still following good practices where they don’t add much difficulty.
|
||||
|
||||
## 1) What the checker does (goals / non-goals)
|
||||
|
||||
### Goals
|
||||
|
||||
The checker verifies that generator output is:
|
||||
|
||||
1. **Parsable** as TCP/IP traffic,
|
||||
2. **Modbus/TCP-valid** at the application level (MBAP + PDU rules),
|
||||
3. **Consistent** in request/response pairing (Transaction ID matching),
|
||||
4. Optionally **matches the expected features** (function code, unit id, quantities, payload size, timing tolerances, etc.).
|
||||
|
||||
### Non-goals (to keep it simple)
|
||||
|
||||
To avoid turning this into a full Wireshark, we deliberately **do not** implement:
|
||||
|
||||
* full TCP stream reassembly (segments split/merged),
|
||||
* full TCP state machine with retransmits/out-of-order handling,
|
||||
* IP/TCP checksum verification by default.
|
||||
|
||||
Instead, we enforce a **generator constraint**: **one Modbus ADU per TCP payload** (no segmentation, no coalescing). This single constraint dramatically reduces checker complexity and is realistic for generated traces.
|
||||
|
||||
> Trade-off: best practice would handle segmentation/coalescing and reassembly; difficulty rises a lot. The “one ADU per TCP payload” rule is the best complexity/benefit lever for this project.
|
||||
|
||||
---
|
||||
|
||||
## 2) Generator output contract (what the checker consumes)
|
||||
|
||||
### Recommended output (MVP-friendly and debuggable)
|
||||
|
||||
**(A) PCAP or PCAPNG file**
|
||||
|
||||
* `trace.pcapng` (or `.pcap`) containing the raw generated packets
|
||||
|
||||
**(B) Sidecar JSONL metadata file**
|
||||
|
||||
* `trace.meta.jsonl` where each line describes the corresponding packet/event (same order)
|
||||
|
||||
This is the easiest way to:
|
||||
|
||||
* reproduce failures,
|
||||
* correlate packet index with expected semantic fields,
|
||||
* produce actionable reports.
|
||||
|
||||
### JSONL schema (minimal + optional)
|
||||
|
||||
**Minimal fields (recommended):**
|
||||
|
||||
* `trace_id` (string/uuid)
|
||||
* `event_id` (monotonic integer)
|
||||
* `pcap_index` (or implicit by line number)
|
||||
* `ts_ns` timestamp
|
||||
* `direction` (`"c2s"` or `"s2c"`)
|
||||
* `flow` (src/dst ip/port)
|
||||
|
||||
**Optional `expected` block (for feature-fidelity checks):**
|
||||
|
||||
* `expected.modbus.transaction_id`, `unit_id`, `function_code`, and `expected.fields` (names matching your descriptor JSON).
|
||||
|
||||
Example line:
|
||||
|
||||
```json
|
||||
{
|
||||
"trace_id": "c7f1...",
|
||||
"event_id": 42,
|
||||
"pcap_index": 42,
|
||||
"ts_ns": 1736451234567890123,
|
||||
"direction": "c2s",
|
||||
"flow": {"src_ip":"10.0.0.10","src_port":51012,"dst_ip":"10.0.0.20","dst_port":502},
|
||||
"expected": {
|
||||
"modbus": {"transaction_id": 513, "unit_id": 1, "function_code": 3},
|
||||
"fields": {"starting_address": 0, "quantity": 10}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Trade-off: best practice is “self-describing PCAP” (pcapng custom blocks, or embedding metadata); difficulty higher. JSONL sidecar is dead simple and works well.
|
||||
|
||||
---
|
||||
|
||||
## 3) Workflow (starting from generator output)
|
||||
|
||||
### Step 0 — Load inputs
|
||||
|
||||
1. Read `trace.meta.jsonl` into a lightweight iterator (don’t load all if trace is huge).
|
||||
2. Open `trace.pcapng` and stream packets in order.
|
||||
|
||||
### Step 1 — Align packets and metadata
|
||||
|
||||
For each packet index `i`:
|
||||
|
||||
* read packet `i` from PCAP
|
||||
* read metadata line `i` from JSONL
|
||||
If mismatch (missing line/packet), record a **Fatal** alignment error and stop (or continue with “best effort”, your call).
|
||||
|
||||
### Step 2 — Decode packet and extract TCP payload
|
||||
|
||||
Decode:
|
||||
|
||||
* link layer (Ethernet/SLL/RAW depending on PCAP linktype),
|
||||
* IPv4/IPv6,
|
||||
* TCP,
|
||||
* extract TCP payload bytes.
|
||||
|
||||
Minimal checks:
|
||||
|
||||
* packet parses,
|
||||
* TCP payload length > 0 when direction indicates Modbus message,
|
||||
* port 502 is present on either side (configurable if you generate non-502).
|
||||
|
||||
### Step 3 — Parse Modbus/TCP ADU
|
||||
|
||||
Assuming payload contains exactly one ADU:
|
||||
|
||||
* parse MBAP (7 bytes) + PDU
|
||||
* validate basic MBAP invariants
|
||||
* parse function code and PDU data
|
||||
* decide request vs response based on `direction`
|
||||
* parse PDU data using descriptor map (your reusable part)
|
||||
|
||||
### Step 4 — Stateful consistency checks
|
||||
|
||||
Maintain per-flow state:
|
||||
|
||||
* request/response pairing by `(transaction_id, unit_id)`
|
||||
* outstanding request table with timeout/window limits
|
||||
|
||||
### Step 5 — Feature-fidelity checks (optional)
|
||||
|
||||
If `expected` exists in JSONL:
|
||||
|
||||
* compare decoded modbus header + parsed fields with expected values
|
||||
* compare sizes and (optionally) timing with tolerances
|
||||
|
||||
### Step 6 — Emit report
|
||||
|
||||
Output:
|
||||
|
||||
* `report.json` with summary + per-finding samples (packet indices, flow key, reason, extracted fields)
|
||||
* optional `report.txt` for quick reading
|
||||
|
||||
---
|
||||
|
||||
## 4) Reusing your existing parser (what to keep, what to adjust)
|
||||
|
||||
You already have:
|
||||
|
||||
* A descriptor model (`Config/FunctionDescriptor/FieldDescriptor/FieldType`)
|
||||
* A function that returns a JSON representation with the shape the checker wants (`unit`, `function`, optional `exception`, `fields`)
|
||||
|
||||
### 4.1 What is immediately reusable
|
||||
|
||||
**Highly reusable for the checker:**
|
||||
|
||||
* Descriptor loading (serde)
|
||||
* Field decoding logic (length/length_from, scale, enum_map)
|
||||
* The “JSON output” idea for reporting and debugging
|
||||
|
||||
### 4.2 Small design adjustment to make reuse clean (recommended)
|
||||
|
||||
Your checker will naturally see **raw TCP payload bytes**. So the lowest-friction integration is:
|
||||
|
||||
* Implement a tiny **MBAP parser** in the checker:
|
||||
|
||||
* returns `(transaction_id, protocol_id, length, unit_id, function_code, pdu_data)`
|
||||
* Then call your descriptor-based decoder on `pdu_data` (bytes **after** function code)
|
||||
|
||||
Your doc shows the parser conceptually returns JSON with `fields` and supports request vs response descriptors , which maps perfectly to `direction`.
|
||||
|
||||
**Suggested public entrypoint to expose from your parser module:**
|
||||
|
||||
* `parse_with_descriptor(pdu_data: &[u8], unit: u8, function: u8, fields: &Vec<FieldDescriptor>) -> Result<Value, String>`
|
||||
|
||||
If it’s currently private, just make it `pub(crate)` or `pub` and reuse it. This avoids binding the checker to `sawp_modbus::Message` and keeps implementation simple.
|
||||
|
||||
> Trade-off: best practice would be to return a typed struct + typed errors; easier to maintain long term but more refactor work. For your “don’t make it hard” requirement, keeping JSON output + simple error types is totally fine for the first version.
|
||||
|
||||
### 4.3 How the checker chooses which descriptor to use
|
||||
|
||||
* If `direction == c2s` → request descriptor
|
||||
* If `direction == s2c` → response descriptor
|
||||
This matches the intent of having `request` and `response` descriptor vectors in your model .
|
||||
|
||||
---
|
||||
|
||||
## 5) Checker internal design (simple but extensible)
|
||||
|
||||
### 5.1 Core data structures
|
||||
|
||||
* `FlowKey { src_ip, src_port, dst_ip, dst_port, ip_version }`
|
||||
* `PacketCtx { trace_id, event_id, pcap_index, ts_ns, direction, flow }`
|
||||
* `DecodedModbus { transaction_id, protocol_id, length, unit_id, function_code, is_exception, exception_code?, pdu_data, parsed_fields_json? }`
|
||||
|
||||
### 5.2 “Rules” model (optional, but keeps code tidy)
|
||||
|
||||
Instead of huge if/else blocks, implement a few rules that return findings:
|
||||
|
||||
* `RuleMbapValid`
|
||||
* `RuleFunctionPduWellFormed` (basic length sanity)
|
||||
* `RuleTxIdPairing`
|
||||
* `RuleExpectedMatch` (only if sidecar has expected)
|
||||
|
||||
If you don’t want a formal trait system initially, just implement these as functions that append to a `Vec<Finding>`.
|
||||
|
||||
### 5.3 Findings + severity
|
||||
|
||||
Use a compact severity scale:
|
||||
|
||||
* `Fatal`: cannot parse / cannot continue reliably
|
||||
* `Error`: protocol invalid
|
||||
* `Warn`: unusual but maybe acceptable
|
||||
* `Info`: stats
|
||||
|
||||
A finding should include:
|
||||
|
||||
* `pcap_index`, `event_id`, `flow`, `severity`, `code`, `message`
|
||||
* optional `observed` and `expected` snippets
|
||||
|
||||
---
|
||||
|
||||
## 6) What the checker validates (MVP vs stricter)
|
||||
|
||||
### MVP validations (recommended first milestone)
|
||||
|
||||
1. PCAP + JSONL aligned
|
||||
2. Parse Ethernet/IP/TCP and extract payload
|
||||
3. MBAP:
|
||||
|
||||
* payload length ≥ 7
|
||||
* length field consistency (basic)
|
||||
4. PDU:
|
||||
|
||||
* function code exists
|
||||
* exception handling if `fc & 0x80 != 0`
|
||||
5. Descriptor parse success (request/response based on direction)
|
||||
6. Transaction pairing:
|
||||
|
||||
* every response matches an outstanding request by transaction_id/unit_id
|
||||
* no duplicate outstanding txid unless you allow it
|
||||
|
||||
### “Strict mode” additions (still reasonable)
|
||||
|
||||
* enforce unit_id range (if you want)
|
||||
* enforce function-code-specific invariants using parsed fields
|
||||
|
||||
* e.g., `byte_count == 2 * quantity` for register reads/writes (if present in descriptor)
|
||||
* timeouts:
|
||||
|
||||
* response must arrive within configured window
|
||||
|
||||
### Heavy features (avoid unless needed)
|
||||
|
||||
* TCP reassembly and multi-ADU per segment
|
||||
* checksum verification
|
||||
* handling retransmits/out-of-order robustly
|
||||
|
||||
---
|
||||
|
||||
## 7) Dependencies (crates) for the checker
|
||||
|
||||
### Minimal set (keeps implementation easy)
|
||||
|
||||
* **PCAP reading**
|
||||
|
||||
* `pcap` (libpcap-backed; you already use it in your codebase)
|
||||
* **Packet decoding**
|
||||
|
||||
* `pnet_packet` (you already use `pnet` patterns)
|
||||
* **Config + sidecar + report**
|
||||
|
||||
* `serde`, `serde_json`
|
||||
* **Errors + logging**
|
||||
|
||||
* `anyhow` (fast to integrate) and/or `thiserror` (nicer structured errors)
|
||||
* `tracing`, `tracing-subscriber`
|
||||
* **Utilities**
|
||||
|
||||
* `hashbrown` (optional; std HashMap is fine)
|
||||
* `hex` (useful for debug/trailing bytes like your parser does)
|
||||
|
||||
### If you want to reduce external requirements (optional alternative)
|
||||
|
||||
* Replace `pcap` with `pcap-file` (pure Rust; no libpcap dependency)
|
||||
* Replace `pnet` with `etherparse` (often simpler APIs)
|
||||
|
||||
> Trade-off: “best practice” for portability is pure Rust (`pcap-file` + `etherparse`).
|
||||
> “Best practice” for least effort *given your current code* is reusing `pcap` + `pnet`.
|
||||
|
||||
---
|
||||
|
||||
## 8) Suggested project layout (simple)
|
||||
|
||||
```
|
||||
checker/
|
||||
src/
|
||||
main.rs # CLI entry
|
||||
config.rs # descriptor loading
|
||||
meta.rs # JSONL reader structs
|
||||
pcap_in.rs # pcap streaming
|
||||
decode.rs # ethernet/ip/tcp extract payload
|
||||
mbap.rs # Modbus/TCP MBAP parsing
|
||||
modbus_desc.rs # reuse your parse_with_descriptor + types
|
||||
state.rs # outstanding tx table
|
||||
validate.rs # main validation pipeline
|
||||
report.rs # report structs + JSON output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) Practical implementation tips (to keep it from getting “hard”)
|
||||
|
||||
1. **Enforce generator constraints**:
|
||||
|
||||
* one ADU per TCP payload
|
||||
* no splitting/coalescing
|
||||
This keeps checker complexity low and makes failure reasons obvious.
|
||||
|
||||
2. **Keep JSON output for parsed fields** at first:
|
||||
|
||||
* You already have a clean JSON shape (`unit`, `function`, `fields`)
|
||||
* Great for debugging mismatches with `expected.fields`
|
||||
|
||||
3. **Add strictness as “modes”**:
|
||||
|
||||
* `--mode=mvp | strict`
|
||||
* or config file toggles
|
||||
|
||||
4. **Fail-fast vs best-effort**:
|
||||
|
||||
* For CI or batch filtering, fail-fast on `Fatal` is fine.
|
||||
* For research/debugging, best-effort (continue and collect findings) is more useful.
|
||||
|
||||
---
|
||||
|
||||
16
docs/examples/modbus.json
Normal file
16
docs/examples/modbus.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
"function": 3,
|
||||
"name": "read_holding_registers",
|
||||
"request": [
|
||||
{"name":"starting_address","type":"u16"},
|
||||
{"name":"quantity","type":"u16"}
|
||||
],
|
||||
"response": [
|
||||
{"name":"byte_count","type":"u8"},
|
||||
{"name":"registers","type":"bytes","length_from":"byte_count"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
31
docs/examples/report.json
Normal file
31
docs/examples/report.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"summary": {
|
||||
"total_packets": 1,
|
||||
"total_findings": 1,
|
||||
"fatal": 0,
|
||||
"error": 1,
|
||||
"warn": 0,
|
||||
"info": 0
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"pcap_index": 0,
|
||||
"event_id": 1,
|
||||
"severity": "error",
|
||||
"code": "expected_field_mismatch",
|
||||
"message": "Field mismatch for quantity",
|
||||
"flow": {
|
||||
"src_ip": "10.0.0.10",
|
||||
"src_port": 51012,
|
||||
"dst_ip": "10.0.0.20",
|
||||
"dst_port": 502
|
||||
},
|
||||
"observed": {
|
||||
"field": "quantity",
|
||||
"observed": 1,
|
||||
"expected": 2
|
||||
},
|
||||
"expected": null
|
||||
}
|
||||
]
|
||||
}
|
||||
1
docs/examples/trace.meta.jsonl
Normal file
1
docs/examples/trace.meta.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"trace_id":"example-trace","event_id":1,"pcap_index":0,"ts_ns":1736451234567890000,"direction":"c2s","flow":{"src_ip":"10.0.0.10","src_port":51012,"dst_ip":"10.0.0.20","dst_port":502},"expected":{"modbus":{"transaction_id":513,"unit_id":1,"function_code":3},"fields":{"starting_address":0,"quantity":2}}}
|
||||
Reference in New Issue
Block a user