{
  "markdown": "# dsct\n\n[![crates.io](https://img.shields.io/crates/v/dsct.svg)](https://crates.io/crates/dsct)\n[![docs.rs](https://docs.rs/dsct/badge.svg)](https://docs.rs/dsct)\n[![MSRV](https://img.shields.io/badge/rustc-1.88+-blue.svg)](https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/)\n[![CI](https://github.com/higebu/dsct/actions/workflows/ci.yml/badge.svg)](https://github.com/higebu/dsct/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/github/higebu/dsct/graph/badge.svg?token=EaeOxnsedN)](https://codecov.io/github/higebu/dsct)\n\n`dsct` is a packet dissector CLI for LLMs and large captures.\n\nIt is built around two ideas:\n\n- machine-readable output by default\n- predictable memory use on big `pcap` / `pcapng` files\n\n`dsct read` streams packet records as JSONL, `dsct stats` scans captures in a single pass, and the optional TUI opens large files with memory mapping and on-demand dissection instead of decoding the whole capture up front.\n\n## Why dsct\n\n### LLM-friendly by default\n\n- `dsct read` emits JSONL packet records\n- `dsct stats`, `dsct list`, `dsct fields`, `dsct version`, and `dsct schema` emit JSON\n- errors, warnings, and progress updates are structured JSON on stderr\n- capabilities and schemas can be discovered from the CLI itself\n\n### Works well on large captures\n\n- `read` and `stats` process captures one packet at a time\n- stdin is supported, so `tcpdump -w - | dsct ...` works naturally\n- no human-oriented table parsing is required before automation can start\n\n### MCP server built in\n\n`dsct mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio. AI agents can call tools like `dsct_read_packets` and `dsct_get_stats` directly, without shelling out to the CLI.\n\n### Low-memory TUI for large files\n\nThe optional TUI is designed for large captures too:\n\n- capture files are opened with memory-mapped I/O\n- indexing starts from packet headers instead of fully decoding every packet\n- packet list rows are dissected on demand for visible rows\n- the selected packet is decoded in detail only when needed\n- the hex view reads directly from the mapped file\n\n## Installation\n\nCLI only:\n\n```bash\ncargo install dsct\n```\n\nWith the optional TUI:\n\n```bash\ncargo install dsct --features tui\n```\n\n```bash\nbrew install higebu/tap/dsct\n```\n\n## AI coding agent plugins\n\nInstall as a plugin via the marketplace to get the MCP server and the\n`analyze-packets` skill automatically:\n\n**Claude Code**\n\n```bash\nclaude plugin marketplace add higebu/dsct\nclaude plugin install dsct@dsct\n```\n\n**GitHub Copilot CLI**\n\n```bash\ncopilot plugin marketplace add higebu/dsct\ncopilot plugin install dsct@dsct\n```\n\n**OpenAI Codex CLI**\n\nAdd the MCP server, then install the `analyze-packets` skill inside Codex:\n\n```bash\ncodex mcp add dsct -- dsct mcp\n```\n\n```text\n$skill-installer higebu/dsct skills/analyze-packets\n```\n\n**Gemini CLI**\n\n```bash\ngemini extensions install https://github.com/higebu/dsct\n```\n\n## Quick start\n\nGet a capture overview:\n\n```bash\ndsct stats capture.pcap\n```\n\nRead packets as JSONL:\n\n```bash\ndsct read capture.pcap\n```\n\nBy default, `dsct read` outputs at most **1 000 packets**. Use `--count` to\nchange the limit or `--no-limit` to remove it:\n\n```bash\ndsct read capture.pcap --count 50\ndsct read capture.pcap --no-limit\n```\n\nFilter packets:\n\n```bash\ndsct read capture.pcap -f dns --count 10\ndsct read capture.pcap -f \"dns AND dns.qr = 'Query'\"\n```\n\nFilter expressions use SQL syntax with `AND`, `OR`, `NOT`, parentheses, and\ncomparison operators (`=`, `!=`, `>`, `<`, `>=`, `<=`):\n\n```bash\ndsct read capture.pcap -f \"dns OR (tcp AND ipv4.src = '10.0.0.1')\"\ndsct read capture.pcap -f \"tcp.dst_port > 1024 AND NOT dns\"\n```\n\nNested fields are addressed with dots (`dns.questions.name`); the leading\nsegment matches the top-level field and any nested container of that name, so\n`bgp.nlri.route_type` reaches MP_REACH NLRI.\n\nSample evenly across the capture:\n\n```bash\ndsct read capture.pcap --sample-rate 100\ndsct read capture.pcap -f dns --sample-rate 10 --count 50\n```\n\nRead from a pipe:\n\n```bash\ntcpdump -w - -c 1000 | dsct read -\ntcpdump -w - -i eth0 udp port 53 | dsct read - -f dns\n```\n\nInclude the original packet bytes (link-layer included) as a hex string under\n`raw_bytes` for downstream parsing or reconstruction:\n\n```bash\ndsct read capture.pcap --raw-bytes --count 1\n```\n\nSpeed up filter evaluation on large files with `--threads`:\n\n```bash\ndsct read capture.pcap -f \"udp\" --no-limit --threads 4\nDSCT_THREADS=4 dsct read capture.pcap -f \"tcp.dst_port > 1024\" --no-limit\n```\n\n`--threads` distributes dissection and filter evaluation across N worker\nthreads when the filter is stateless (L2–L4 protocols: `tcp`, `udp`, `ipv4`,\netc.).  Filters that require TCP reassembly such as `http`, `dns`, `tls`, and\n`tcp.stream_id` automatically fall back to sequential processing regardless of\n`--threads`.  Stdin input always uses the sequential path.\n\nQuery a capture with SQL (the SQLite index is built on first use and reused\nafterwards):\n\n```bash\ndsct sql capture.pcap \"SELECT number, stack FROM packets WHERE max_depth > 0 LIMIT 10\"\ndsct sql capture.pcap \"SELECT * FROM tcp_segments WHERE flow_id = 0 ORDER BY packet_number\"\ndsct sql capture.pcap --schema\n```\n\nSee [SQL queries](#sql-queries) for the database layout.\n\nInspect available fields and schemas:\n\n```bash\ndsct fields dns\ndsct schema read\n```\n\nOpen the TUI for a large file (when built with `--features tui`):\n\n```bash\ndsct tui capture.pcap\n```\n\nIn the TUI, press `?` to open the built-in help overlay and `q` to quit.\n\n## Typical workflow\n\n```bash\n# 1. Discover supported protocols\ndsct list\n\n# 2. Inspect available filter fields\ndsct fields dns\n\n# 3. Read matching packets as JSONL\ndsct read capture.pcap -f \"dns AND dns.qr = 'Query'\" --count 20\n\n# 4. Get capture-wide statistics\ndsct stats capture.pcap --top-talkers\n```\n\n## Commands\n\n| Command | What it does |\n| --- | --- |\n| `dsct read <FILE>` | Stream packet records as JSONL |\n| `dsct stats <FILE>` | Emit capture statistics as JSON |\n| `dsct index <FILE>` | Build (or refresh) the SQLite index used by `dsct sql` |\n| `dsct sql <FILE> <QUERY>` | Run a read-only SQL query against the capture's SQLite index, rows as JSONL |\n| `dsct list` | List supported protocols as JSON (`name`, `full_name`, `layer`, spec `references`) |\n| `dsct fields [PROTOCOL...]` | List filterable fields as JSON |\n| `dsct schema [COMMAND]` | Show JSON Schema for command output |\n| `dsct version` | Show version and capability information as JSON |\n| `dsct mcp` | Start an MCP server over stdio |\n| `dsct tui <FILE>` | Open the interactive TUI for a capture file (`tui` feature only) |\n\nRun `--help` on any command for the full option list.\n\n## MCP tools\n\n`dsct mcp` exposes the following tools over the Model Context Protocol:\n\n| Tool | Description |\n| --- | --- |\n| `dsct_read_packets` | Dissect packets from a pcap/pcapng capture file. Returns an array of dissected packet objects with protocol layers and fields. |\n| `dsct_get_stats` | Get protocol statistics from a capture file. Returns packet counts, timing, protocol distribution, and optional deep analysis. |\n| `dsct_list_protocols` | List all supported protocols (`name`, `full_name`, `layer`, and spec `references` with `id`/`title`/`url`). |\n| `dsct_list_fields` | List available field names for protocols. `qualified_name` is the path to use in `dsct_read_packets` `filter`/`fields`. |\n| `dsct_get_schema` | Get the JSON schema for command output formats (`read`, `stats` or `sql`). |\n| `dsct_query_sql` | Run a read-only SQL query against the capture's SQLite index (built on first use). Returns result rows plus index status; `schema: true` returns the table layout. |\n\n### Protocol versions\n\nThe server speaks both protocol eras and picks one per request:\n\n- **`2026-07-28`** (stateless): declare the version on every request via\n  `params._meta` (`io.modelcontextprotocol/protocolVersion` and\n  `io.modelcontextprotocol/clientCapabilities` are required), and probe with\n  `server/discover`. `ping` is not served in this era.\n- **`2025-11-25` / `2025-03-26` / `2024-11-05`** (legacy): negotiate via the\n  `initialize` handshake as before.\n\n### Key parameters\n\n**`dsct_read_packets`**: `file` (required), `filter`, `count`, `offset`, `packet_number`, `decode_as`, `esp_sa`, `verbose`, `layers`, `fields`\n\n- `layers`: protocol names to keep in each packet's `layers` array (`\"BGP\"` or\n  `[\"IPv4\", \"TCP\", \"BGP\"]`); `stack` is unaffected.\n- `fields`: qualified field paths to keep (`\"BGP.nlri\"`,\n  `\"BGP.path_attributes.value.nlri.route_type\"`); protocols not listed keep\n  their default fields. The last segment accepts the `default_fields.toml`\n  patterns (`prefix*`, `*suffix`).\n\nBoth are MCP-only and useful for large protocols such as BGP.\n\n**`dsct_query_sql`**: `file` (required), `sql`, `schema`, `tables`, `count`, `db`, `no_build`, `decode_as`, `esp_sa`\n\n- `schema: true` returns a compact list of tables and views; add\n  `tables` (`\"tcp\"` or `[\"tcp\", \"bgp\"]`) for full column detail.\n\n### Configuration example\n\nAdd `dsct` to your MCP client (e.g. Claude Desktop):\n\n```json\n{\n  \"mcpServers\": {\n    \"dsct\": {\n      \"command\": \"dsct\",\n      \"args\": [\"mcp\"]\n    }\n  }\n}\n```\n\n### Default limits\n\nWhen `count` is omitted, `dsct_read_packets` returns at most **1 000 packets**\n(configurable via `DSCT_MCP_DEFAULT_COUNT`). `dsct_get_stats` processes the\nentire capture by default. All tool calls are subject to a per-execution\ntimeout; on timeout the server returns a JSON-RPC error and no partial output\nis sent.\n\n### Environment variables\n\nResource limits can be tuned via environment variables:\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `DSCT_MCP_DEFAULT_COUNT` | 1000 | Default packet count when `count` is not specified |\n| `DSCT_MCP_TIMEOUT` | 300 | Timeout per tool execution in seconds |\n| `DSCT_MCP_WRITE_BUFFER_SIZE` | 65536 | Stdout write buffer size in bytes |\n| `DSCT_MCP_MAX_FILE_SIZE` | 10737418240 | Maximum capture file size in bytes |\n| `DSCT_THREADS` | physical CPU count | Worker threads for `dsct read --filter` (see `--threads`) |\n| `DSCT_CACHE_DIR` | `$XDG_CACHE_HOME/dsct`, else `$HOME/.cache/dsct` | Directory for `dsct sql`/`dsct index` database files (see [SQL queries](#sql-queries)) |\n\n## Output\n\n`dsct read` emits one JSON object per line:\n\n```jsonl\n{\"number\":1,\"timestamp\":\"2024-01-15T10:30:00.123456Z\",\"length\":71,\"original_length\":71,\"stack\":\"Ethernet:IPv4:UDP:DNS\",\"layers\":[{\"protocol\":\"Ethernet\",\"fields\":{\"dst\":\"ff:ff:ff:ff:ff:ff\",\"src\":\"00:11:22:33:44:55\",\"ethertype\":2048,\"ethertype_name\":\"IPv4\"}},{\"protocol\":\"IPv4\",\"fields\":{\"ttl\":64,\"protocol\":17,\"src\":\"10.0.0.1\",\"dst\":\"10.0.0.2\"}},{\"protocol\":\"UDP\",\"fields\":{\"src_port\":12345,\"dst_port\":53}},{\"protocol\":\"DNS\",\"fields\":{\"id\":4660,\"qr\":0,\"opcode\":0,\"rcode\":0,\"questions\":[{\"name\":\"example.com\",\"type\":1,\"class\":1}]}}]}\n```\n\n`dsct sql` emits one JSON object per result row, keyed by column name:\n\n```jsonl\n{\"packet_number\":2,\"depth\":1,\"carrier_protocol\":\"VXLAN\",\"carrier_layer_index\":3}\n```\n\nSQLite `INTEGER` and `REAL` values become JSON numbers, `TEXT` becomes a\nstring, `BLOB` becomes a lowercase hex string and `NULL` becomes `null`.\n\nThe other commands emit a single JSON object or array on stdout.\n\n## SQL queries\n\n`dsct sql` dissects a capture once, stores every layer in a SQLite database,\nand answers `SELECT` queries against it. The database is built on the first\nquery (or explicitly with `dsct index`) and reused as long as the capture, the\ndsct version and the dissection options are unchanged; otherwise it is rebuilt\nand a `{\"warning\":{\"code\":\"index_rebuilt\",...}}` line is written to stderr.\n\n```bash\ndsct index capture.pcap                 # build now (prints {\"type\":\"index\",...})\ndsct sql capture.pcap --schema          # tables, columns, descriptions, hints\ndsct sql capture.pcap \"SELECT protocol, COUNT(*) AS n FROM layers GROUP BY protocol ORDER BY n DESC\"\ndsct sql ~/.cache/dsct/capture.pcap-3f2a9c1d8e4b0716.dsct.sqlite \"SELECT COUNT(*) FROM packets\"  # query an index directly\ntcpdump -w - -c 1000 | dsct sql - --db /tmp/live.sqlite \"SELECT * FROM conversations\"\n```\n\n- The index lives in `$DSCT_CACHE_DIR`, else `$XDG_CACHE_HOME/dsct`, else\n  `$HOME/.cache/dsct`, as `<name>-<hash of the capture path>.dsct.sqlite`;\n  override with `--db PATH`. Reading from stdin requires `--db` and always\n  rebuilds.\n- `--schema` prints the table and view definitions; `--tables tcp,udp` narrows\n  it.\n- Like `dsct read`, output stops after **1 000 rows** by default; use\n  `--count N` or `--no-limit`.\n- Expect the index to take roughly one to three times the size of the capture.\n  Flow tracking keeps one small entry per conversation in memory while building.\n\n### Tables\n\n| Table / view | Contents |\n| --- | --- |\n| `packets` | One row per packet: `number`, `timestamp`, `ts`, lengths, `link_type`, `stack`, `max_depth`, `dissect_error` |\n| `layers` | One row per dissected layer: `packet_number`, `layer_index`, `depth`, `protocol`, `protocol_name`, `offset`, `length` |\n| `<protocol>` | One table per protocol (`ipv4`, `tcp`, `dns`, `gtpv1u`, ...): one row per layer with a column per field, keyed by `packet_number`, `layer_index`, `depth` |\n| `flows` | One row per transport conversation per depth: endpoints, packet/byte counts, first/last packet and time, `tcp_stream_id` |\n| `packet_flows` | Maps transport layers to flows with `direction` (`0` = `addr_a` → `addr_b`, `1` = reverse) |\n| `encapsulations` | View: for every tunnelled depth of a packet, the carrier protocol (`VXLAN`, `GRE`, `GTPv1-U`, ...) |\n| `conversations` | View: `flows` plus `duration_secs` |\n| `tcp_segments` | View: TCP rows joined with `packets`, including `seq_rel`, `ack_rel`, `next_seq`, `payload_len`, `flags_name` |\n\nProtocol table names are the lowercase, alphanumeric form of the protocol name\n(`GTPv1-U` → `gtpv1u`, `HTTP/2` → `http2`). Field columns keep their `dsct read`\nnames; fields with a display name also get a `<name>_name` text column\n(`flags_name`, `ethertype_name`). Quote column names that collide with SQL\nkeywords (`\"type\"`, `\"class\"`, `\"group\"`, `\"offset\"`). Array and object fields\nare stored as JSON text. Run `dsct sql <FILE> --schema` to list everything.\n\n### Encapsulated and nested packets\n\nEvery layer carries an encapsulation `depth`: `0` for the outer packet, `1`\nfor the first tunnelled packet (VXLAN, Geneve, GRE, GTP-U, IP-in-IP, L2TP,\nMPLS, decrypted ESP, ...), `2` for a tunnel inside a tunnel, and so on. Inner\nheaders are ordinary rows in the same protocol tables:\n\n```bash\n# Inner IPv4 headers carried inside tunnels\ndsct sql capture.pcap \"SELECT packet_number, \\\"src\\\", \\\"dst\\\" FROM ipv4 WHERE depth = 1\"\n\n# Which tunnel protocol carries each inner packet\ndsct sql capture.pcap \"SELECT carrier_protocol, COUNT(*) FROM encapsulations GROUP BY carrier_protocol\"\n\n# Inner TCP flows carried over GTP-U, joined with the outer tunnel endpoints\ndsct sql capture.pcap \"SELECT t.packet_number, o.\\\"src\\\" AS outer_src, i.\\\"src\\\" AS inner_src, t.\\\"dst_port\\\" \\\n  FROM tcp t JOIN ipv4 i ON i.packet_number = t.packet_number AND i.depth = t.depth \\\n  JOIN ipv4 o ON o.packet_number = t.packet_number AND o.depth = 0 WHERE t.depth = 1\"\n\n# Nested fields via SQLite JSON functions\ndsct sql capture.pcap \"SELECT p.number, json_extract(q.value, '$.name') AS qname \\\n  FROM dns d JOIN packets p ON p.number = d.packet_number, json_each(d.\\\"questions\\\") q\"\n```\n\n### Following streams and sequences\n\n`tcp`, `udp` and `sctp` rows carry a dsct-assigned `flow_id` (per depth) and a\n`direction`; both directions of a conversation share one id. TCP rows also get\n`payload_len`, `seq_rel` / `ack_rel` (relative to the first segment seen in each\ndirection) and `next_seq`:\n\n```bash\n# Busiest conversations\ndsct sql capture.pcap \"SELECT * FROM conversations ORDER BY bytes DESC LIMIT 10\"\n\n# Follow one TCP stream in order\ndsct sql capture.pcap \"SELECT packet_number, direction, seq_rel, ack_rel, payload_len, flags_name \\\n  FROM tcp_segments WHERE flow_id = 3 ORDER BY packet_number\"\n\n# Retransmissions: a data segment that starts before the end of an earlier segment in the same direction\ndsct sql capture.pcap \"SELECT DISTINCT a.packet_number FROM tcp_segments a JOIN tcp_segments b \\\n  ON a.flow_id = b.flow_id AND a.direction = b.direction AND b.packet_number < a.packet_number \\\n  WHERE a.payload_len > 0 AND b.payload_len > 0 AND a.seq_rel < b.seq_rel + b.payload_len\"\n```\n\n## Supported protocols\n\nThe default build currently includes 50+ protocol dissectors across link, network, transport, tunneling, and application layers.\n\nUse `dsct list` to see the exact protocol set in your build; each entry reports\nthe protocol's `layer` (`link`, `network`, `transport`, `tunnel`,\n`application`) and the specifications it is implemented against.\n\n## Errors\n\nErrors and warnings are emitted as structured JSON on stderr.\n\nExample:\n\n```json\n{\"error\":{\"code\":\"file_not_found\",\"message\":\"failed to open capture file: test.pcap\"}}\n```\n\nExit codes:\n\n| Code | Meaning |\n| --- | --- |\n| `0` | Success |\n| `1` | General error |\n| `2` | Invalid arguments (including rejected or malformed SQL queries) |\n| `3` | File not found or permission denied |\n| `4` | Invalid capture format |\n\n## License\n\nLicensed under either of [Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.\n",
  "bytes": 17327,
  "sha": "9efa929eded3285cdcc0f4bd0422e57ffe7bba8d9757daa546a3fb7ede5c54fd",
  "repo_slug": "higebu/dsct",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_higebu_dsct_ee22d2b1/readme"
}