vanken 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +26 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +10 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/lib/vanken/app/capture_controller.rb +66 -1
  9. data/lib/vanken/app/document.rb +102 -39
  10. data/lib/vanken/app/document_analysis.rb +75 -0
  11. data/lib/vanken/app/document_jobs.rb +52 -9
  12. data/lib/vanken/app/expert_info.rb +26 -0
  13. data/lib/vanken/app/io_graph.rb +47 -0
  14. data/lib/vanken/app/navigation.rb +77 -0
  15. data/lib/vanken/app/search_job.rb +62 -0
  16. data/lib/vanken/app/stats_job.rb +45 -0
  17. data/lib/vanken/capture/analyzer.rb +3 -1
  18. data/lib/vanken/capture/analyzer_process.rb +9 -6
  19. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  20. data/lib/vanken/capture/filter_worker.rb +8 -2
  21. data/lib/vanken/capture/helper_main.rb +10 -3
  22. data/lib/vanken/capture/helper_options.rb +6 -2
  23. data/lib/vanken/capture/launcher.rb +1 -0
  24. data/lib/vanken/config/analysis_settings.rb +58 -0
  25. data/lib/vanken/config/columns.rb +138 -0
  26. data/lib/vanken/config/messages.rb +105 -0
  27. data/lib/vanken/config/preferences.rb +34 -8
  28. data/lib/vanken/config/profiles.rb +74 -0
  29. data/lib/vanken/config/sessions.rb +69 -0
  30. data/lib/vanken/config/yaml_file.rb +36 -0
  31. data/lib/vanken/core/coloring.rb +86 -0
  32. data/lib/vanken/core/frame_store.rb +2 -2
  33. data/lib/vanken/core/stores.rb +12 -0
  34. data/lib/vanken/gateway/detail_builder.rb +1 -1
  35. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  36. data/lib/vanken/gateway/dissector.rb +31 -6
  37. data/lib/vanken/gateway/exporter.rb +125 -0
  38. data/lib/vanken/gateway/file_writer.rb +26 -2
  39. data/lib/vanken/gateway/interfaces.rb +59 -0
  40. data/lib/vanken/gateway/resolver.rb +131 -0
  41. data/lib/vanken/gateway/statistics.rb +83 -0
  42. data/lib/vanken/gateway/stream.rb +159 -0
  43. data/lib/vanken/ui/actions.rb +54 -28
  44. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  45. data/lib/vanken/ui/application.rb +183 -9
  46. data/lib/vanken/ui/coloring_operations.rb +104 -0
  47. data/lib/vanken/ui/column_operations.rb +115 -0
  48. data/lib/vanken/ui/dialogs.rb +41 -26
  49. data/lib/vanken/ui/file_operations.rb +17 -12
  50. data/lib/vanken/ui/filter_operations.rb +43 -15
  51. data/lib/vanken/ui/main_view.rb +74 -29
  52. data/lib/vanken/ui/navigation_operations.rb +86 -0
  53. data/lib/vanken/ui/packet_source.rb +33 -1
  54. data/lib/vanken/ui/selection.rb +8 -2
  55. data/lib/vanken/ui/settings_operations.rb +233 -0
  56. data/lib/vanken/version.rb +1 -1
  57. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  58. data/sig/generated/vanken/app/document.rbs +23 -1
  59. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  61. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  62. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  63. data/sig/generated/vanken/app/navigation.rbs +31 -0
  64. data/sig/generated/vanken/app/search_job.rbs +21 -0
  65. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  66. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  67. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  68. data/sig/generated/vanken/config/columns.rbs +37 -0
  69. data/sig/generated/vanken/config/messages.rbs +14 -0
  70. data/sig/generated/vanken/config/preferences.rbs +1 -1
  71. data/sig/generated/vanken/config/profiles.rbs +29 -0
  72. data/sig/generated/vanken/config/sessions.rbs +17 -0
  73. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  74. data/sig/generated/vanken/core/coloring.rbs +52 -0
  75. data/sig/generated/vanken/core/stores.rbs +4 -0
  76. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  77. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  78. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  79. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  80. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  81. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  82. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  83. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  84. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  85. data/sig/generated/vanken/ui/application.rbs +23 -1
  86. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  87. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  88. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  89. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  90. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  91. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  92. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  93. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  94. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  95. metadata +51 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bfbb1b288a09cb5fb170bed462adb00f4ae86aa21ee62c247e9e2cefe2aaa97
4
- data.tar.gz: 3f8f754621be04e6fc2a4dac72abae4c10f57a8d3524f018c67825abe18b4e8a
3
+ metadata.gz: 3ff6f270de34fbee27bb93839749a5105ece2667ef59e3281c28e37d5dee782a
4
+ data.tar.gz: cfb0118a6e6c0b1b6a026ca8254e58b2e20f260b11e75bedbbce21a6aa47755b
5
5
  SHA512:
6
- metadata.gz: bc1c63f6df229fbb85db6e00cd69059faa16a69f91469f5cd8a3d7d6ad7b8e0bcce3068ed31ebb4533c77906188b3448026a6e5a1cfe581a2d1f5d4fb058cce0
7
- data.tar.gz: 370e198b35f3c4bb080f773b026ae6005c3457bd4412d9be8b13e1ea868c182b0919cf8d4b041325231a44f1ec8bf2d556dc15eb01ba1bbff47893cde91c6d49
6
+ metadata.gz: d7913b548c7ce8fd8bb0531542b70d633c1c06525bb2be762230661ec2d2691a233c81116376ab3c6fa0857db76c8796c4e585271431fd6025af7c130fc55fab
7
+ data.tar.gz: 1de08c7960be93a43239e88564f3267622406597909a7bd18d7ca59f5bf77dab29526da4b2852405cf711a2066342bc332bad70121ad9f67bead285eb9b6812b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Add editable packet coloring, four search modes, marks, ignored packets, time references, and conversation navigation.
6
+ - Add Follow TCP Stream with retransmission handling, missing-data indicators, searching, saving, and stream exclusion.
7
+ - Add protocol hierarchy, conversations, endpoints, expert information, capture properties, and filtered I/O graphs.
8
+ - Add typed custom columns, Decode As rules, trusted Ruby dissector plugins, packet-range exports, and JSON, NDJSON, text, and CSV exports.
9
+ - Add the command palette, profile-based preferences, Japanese and English screens, high-contrast themes, and keyboard operation in the terminal UI.
10
+ - Add automatic capture stopping, rotating pcapng files, interface traffic graphs, optional asynchronous address resolution, and interrupted-capture recovery.
11
+ - Accelerate supported address and port display filters while preserving ordinary evaluation for other packets and dissector extensions.
12
+ - Preserve captured packets awaiting analysis when saving, and retain filter results and current selection through reanalysis.
13
+
14
+ Live acquisition can outpace analysis on slower systems. Captured packets remain available while queued analysis finishes; redraw latency on measured Linux systems remains above the design target. See [measured performance](https://github.com/ydah/vanken/blob/main/docs/performance.md).
15
+
16
+ Requires Ruby 3.3 or newer. Live capture supports Linux and macOS; Windows supports file inspection. See [capture permissions](https://github.com/ydah/vanken/blob/main/packaging/README.md).
17
+
3
18
  ## 0.1.0
4
19
 
5
20
  Initial release.
data/README.md CHANGED
@@ -10,21 +10,20 @@ Packet acquisition runs in a separate helper; ordered packet analysis runs in an
10
10
  - Linux with a desktop session, or macOS. File inspection is also available on Windows; live capture uses Linux packet sockets or macOS BPF.
11
11
  - Linux desktop dependencies: Vulkan loader and drivers, fonts, and `zenity` for native file dialogs. On Ubuntu: `sudo apt install libvulkan1 mesa-vulkan-drivers fonts-dejavu-core fonts-noto-cjk zenity`.
12
12
 
13
- ## Run from source
13
+ ## Install
14
14
 
15
15
  ```sh
16
- git clone https://github.com/ydah/vanken.git
17
- cd vanken
18
- bundle install
19
- bundle exec exe/vanken capture.pcapng
16
+ gem install vanken
17
+ vanken capture.pcapng
20
18
  ```
21
19
 
22
- Initial RubyGems publication is pending. To install a locally built gem:
20
+ For a source checkout:
23
21
 
24
22
  ```sh
25
- gem build --strict vanken.gemspec
26
- gem install ./vanken-0.1.0.gem
27
- vanken capture.pcapng
23
+ git clone https://github.com/ydah/vanken.git
24
+ cd vanken
25
+ bundle install
26
+ bundle exec exe/vanken capture.pcapng
28
27
  ```
29
28
 
30
29
  Strict gem builds require RubyGems 4.0.16 or newer; older versions reject the pinned redhound prerelease with a recommendation warning.
@@ -43,10 +42,24 @@ frame.len > 1000 && !udp
43
42
 
44
43
  Vanken display filters (VDF) and acquisition filters (BPF) are separate languages. See [display filters](docs/filters.md) for operators, types, and repeated fields.
45
44
 
45
+ ## Analysis
46
+
47
+ Use the menus or the command palette (Ctrl+Shift+K; Command+Shift+K in the macOS desktop application) to find actions. Search by display filter, hexadecimal bytes, string, or regular expression. Mark and ignore packets, change the time reference, and move between packets in the same conversation. Packet and field context menus expose filter and analysis actions.
48
+
49
+ Coloring rules can be edited, reordered, imported, and exported. Add a protocol field as a custom column from the details tree, then change its position, width, or visibility in the column editor. Custom columns sort by their field types.
50
+
51
+ Follow TCP Stream reconstructs each direction, reports missing data, and supports ASCII, hexadecimal, and raw views, searching, saving, and excluding a stream. Statistics include protocol hierarchy, conversations, endpoints, and packet properties. I/O graphs accept display filters per series and intervals from 0.01 to 60 seconds. Expert information links diagnostics to their packets.
52
+
53
+ Decode As rules and registered Ruby dissector plugins are applied to packet summaries, details, and filter workers. Plugins require an explicit trust confirmation before loading. Save captures as pcap or pcapng, export selected ranges, or export dissections as JSON, NDJSON, text, or CSV.
54
+
55
+ Preferences include Japanese and English, dark/light/system/high-contrast themes, analysis limits, and optional asynchronous address resolution. Profiles keep preferences, columns, coloring, bookmarks, Decode As rules, and plugins separate; recent files and window geometry are shared. Stop capture before switching profiles or changing dissectors. Analysis preferences changed during loading or capture are applied when it finishes. Interrupted captures can be recovered or discarded on the next start.
56
+
46
57
  ## Capture
47
58
 
48
59
  Choose Start, select an interface, and optionally set a BPF acquisition filter. Stop preserves captured packets for inspection and saving. Vanken asks before discarding an unsaved capture.
49
60
 
61
+ Capture options include automatic stopping by packet count, duration, or byte count, and rotating pcapng files by size or time with a bounded file count. The welcome screen shows interface traffic rates. Saving includes all durable packets even while analysis is still catching up.
62
+
50
63
  ```sh
51
64
  bundle exec exe/vanken-capture --list-interfaces
52
65
  bundle exec exe/vanken-capture --check --interface lo
@@ -64,6 +77,8 @@ bundle exec exe/vanken --headless --smoke
64
77
 
65
78
  `--no-yjit` disables default YJIT activation. `--debug` enables debug logging. Preferences use safe YAML in the platform's user configuration directory; logs rotate without retaining raw packet bytes.
66
79
 
80
+ The terminal UI supports opening captures, selecting packets, applying filters, and starting/stopping capture with keyboard actions. Details of keyboard controls, settings files, and analysis limits are in the [usage guide](docs/usage.md).
81
+
67
82
  ## Development
68
83
 
69
84
  ```sh
@@ -75,9 +90,9 @@ bundle exec ruby script/benchmark.rb
75
90
  script/capture-ci.sh
76
91
  ```
77
92
 
78
- To develop with a sibling Zaniah checkout, set `VANKEN_ZANIAH_PATH=../zaniah` when running Bundler. Production dependencies are redhound `2.0.0.rc2` and Zaniah `~> 0.12.3`, including fixes for growing memory use and repeated style allocations during redraws.
93
+ To develop with a sibling Zaniah checkout, set `VANKEN_ZANIAH_PATH=../zaniah` when running Bundler. Production dependencies are redhound `2.0.0.rc2` and Zaniah `~> 0.12.4`.
79
94
 
80
- See [upstream contracts](docs/upstream.md), [measured performance](docs/performance.md), and [release procedure](docs/releases.md). Version 0.1 implements the file inspection, acquisition, and display filter milestones. Statistics, stream following, coloring, profiles, and later extensions are scheduled for subsequent releases.
95
+ See [upstream contracts](docs/upstream.md), [measured performance](docs/performance.md), and [release procedure](docs/releases.md). Live acquisition can outpace analysis, and redraw latency on slower Linux machines remains above the design target; captured packets are retained while queued analysis finishes.
81
96
 
82
97
  ## License
83
98
 
@@ -0,0 +1,67 @@
1
+ schema_version: 1
2
+ rules:
3
+ - name: Bad TCP
4
+ filter: tcp.analysis.retransmission || tcp.analysis.out_of_order || tcp.analysis.lost_segment || tcp.analysis.duplicate_ack || tcp.analysis.zero_window
5
+ light: { fg: "#F78787", bg: "#121212" }
6
+ dark: { fg: "#F78787", bg: "#1A1A1A" }
7
+ enabled: true
8
+ - name: Malformed
9
+ filter: expert.severity == error
10
+ light: { fg: "#FFFC9C", bg: "#A40000" }
11
+ dark: { fg: "#FFFC9C", bg: "#7A0000" }
12
+ enabled: true
13
+ - name: Checksum Errors
14
+ filter: expert.code == "bad_checksum"
15
+ light: { fg: "#F78787", bg: "#121212" }
16
+ dark: { fg: "#F78787", bg: "#1A1A1A" }
17
+ enabled: true
18
+ - name: TCP RST
19
+ filter: tcp.flags.rst
20
+ light: { fg: "#FFFC9C", bg: "#A40000" }
21
+ dark: { fg: "#FFFC9C", bg: "#7A0000" }
22
+ enabled: true
23
+ - name: TCP SYN/FIN
24
+ filter: tcp.flags.syn || tcp.flags.fin
25
+ light: { fg: "#12272E", bg: "#A0A0A0" }
26
+ dark: { fg: "#E0E0E0", bg: "#4A4A4A" }
27
+ enabled: true
28
+ - name: HTTP
29
+ filter: http
30
+ light: { fg: "#12272E", bg: "#E4FFC7" }
31
+ dark: { fg: "#E4FFC7", bg: "#25331A" }
32
+ enabled: true
33
+ - name: TLS
34
+ filter: tls
35
+ light: { fg: "#12272E", bg: "#D6E8FF" }
36
+ dark: { fg: "#D6E8FF", bg: "#1E2A3A" }
37
+ enabled: true
38
+ - name: DNS
39
+ filter: dns
40
+ light: { fg: "#12272E", bg: "#C9F0FF" }
41
+ dark: { fg: "#C9F0FF", bg: "#16303A" }
42
+ enabled: true
43
+ - name: ICMP
44
+ filter: icmp || icmpv6
45
+ light: { fg: "#12272E", bg: "#FCE0FF" }
46
+ dark: { fg: "#FCE0FF", bg: "#3A2440" }
47
+ enabled: true
48
+ - name: ARP
49
+ filter: arp
50
+ light: { fg: "#12272E", bg: "#FAF0D7" }
51
+ dark: { fg: "#FAF0D7", bg: "#3A331F" }
52
+ enabled: true
53
+ - name: Broadcast
54
+ filter: eth.dst == ff:ff:ff:ff:ff:ff
55
+ light: { fg: "#BABDB6", bg: "#FFFFFF" }
56
+ dark: { fg: "#9A9D96", bg: "#2A2A2A" }
57
+ enabled: true
58
+ - name: UDP
59
+ filter: udp
60
+ light: { fg: "#12272E", bg: "#DAEEFF" }
61
+ dark: { fg: "#DAEEFF", bg: "#1C2833" }
62
+ enabled: true
63
+ - name: TCP
64
+ filter: tcp
65
+ light: { fg: "#12272E", bg: "#E7E6FF" }
66
+ dark: { fg: "#E7E6FF", bg: "#24233A" }
67
+ enabled: true
data/docs/performance.md CHANGED
@@ -111,3 +111,13 @@ The final single-run check with public Zaniah 0.12.3 and the same Ruby, platform
111
111
  Before visible-row batching, the same operation failed its first selection at 105.252 ms. A separate diagnostic reproduced a 107.234 ms background queue wait behind 11 row jobs, while actual detail analysis took 2.677 ms and byte reading 0.129 ms. Nine intermediate renders consumed most of that wait. PacketSource now collects visible-row requests after rendering, fetches them in one background job, and publishes their values in one foreground update. The passing run followed this runtime change; the earlier failure remains part of the evidence.
112
112
 
113
113
  The native and synthetic runs above do not substitute for this five-minute Linux capture test. Varied addresses, large TCP flow sets, reassembly, and plugins are outside the repeated-UDP benchmark's scope.
114
+
115
+ ## Analysis extensions and cBPF
116
+
117
+ The address/port optimization uses the public verified capture-filter compiler for eligible complete Ethernet IPv4/IPv6 TCP/UDP packets. It falls back to VDF for unsupported expressions, truncation, fragmentation, extension headers, common UDP tunnels, other link types, Decode As, or plugins. This preserves the fields seen by the dissector.
118
+
119
+ On 2026-10-02, Ruby 4.0.6 with YJIT on arm64 Darwin compared the same 20,000 complete Ethernet/IPv4/UDP frames and expression `ip.addr == 192.0.2.1 && udp.port == 54321`. Three sequential samples per evaluator all matched 20,000 packets. Median stateless VDF dissection/evaluation took 0.339384 s, and the eligible cBPF path took 0.020937 s: 16.21 times faster. This comparison excludes file IO and worker startup. Run `bundle exec ruby --yjit script/filter-performance.rb 20000` to repeat it.
120
+
121
+ A separate one-million-packet run of the production document measured 0.5529 s for the existing fast filter, 6.2598 s for the four-worker `ip.ttl == 64` scan, and 2.7721 s for the four-worker address/port cBPF scan. Each matched all one million packets. Analysis took 115.6605 s (8,646 packets/s), with 67.2 B/frame retained parent heap before filtering and 81.08 B/frame after the worker scan. This run overlapped integration and UI tests and is not an isolated comparison with the earlier analysis-throughput result. RSS sampling was unavailable and is recorded as null; these heap figures do not establish the RSS target.
122
+
123
+ The I/O graph check seeds actual frame metadata, packed columns, and annotations for 200,000 TCP frames, then times two series at each supported interval. It excludes packet parsing. The observed interval rebuilds took 0.30–0.33 s, each below the one-second target, with every packet counted in both series. Run `bundle exec ruby --yjit script/analysis-performance.rb` to repeat it. Nightly validation retains the one-million-packet benchmark, evaluator comparison, graph timings, and fuzz results as workflow artifacts.
data/docs/releases.md CHANGED
@@ -4,12 +4,10 @@ Work is committed on main. Release notes and CHANGELOG entries include only user
4
4
 
5
5
  Before tagging, run `bundle exec rake`, verify generated RBS is current, run privileged capture and benchmarks, perform native smoke, and build the gem strictly. Version, tag, gemspec dependencies, and lockfile must agree. Exclude captures, spools, test artifacts, local dependencies, and credentials from the gem.
6
6
 
7
- Initial 0.1.0 notes are exactly `Initial release.`. Publication is paused until the owner publishes the first RubyGem and configures trusted publishing. No initial release tag is pushed automatically.
8
-
9
- For the first publication, run `gem push pkg/vanken-0.1.0.gem`, then configure the publisher identity below. Tagging `v0.1.0` afterwards runs validation and creates the GitHub release; the job skips uploading a version already present on RubyGems.
7
+ Initial 0.1.0 notes are exactly `Initial release.`. Its owner publication and trusted publishing setup are complete. The job skips uploading a version already present on RubyGems.
10
8
 
11
9
  Subsequent `vX.Y.Z` tags trigger `.github/workflows/release.yml`, based on the Canopus release job. It verifies version, runs checks, builds the gem, publishes with RubyGems trusted publishing, and creates GitHub release notes from CHANGELOG.
12
10
 
13
11
  Trusted publisher identity: owner `ydah`, repository `vanken`, workflow `release.yml`, GitHub environment `release`.
14
12
 
15
- The design milestones are 0.1 for file inspection, capture, and filters; 0.2 for coloring, search, stream following, statistics, and analysis extensions; and 0.3 for profiles, localization, permission packaging, recovery UI, and additional capture conveniences. Work stops at the initial publication gate before the later milestones.
13
+ The design milestones are 0.1 for file inspection, capture, and filters; 0.2 for coloring, search, stream following, statistics, and analysis extensions; and 0.3 for profiles, localization, permission packaging, recovery UI, and additional capture conveniences. Some 0.3 foundations are included with 0.2 so analysis extensions can share profiles and settings. The 0.3 release completes the administrator setup command and includes permission packaging as downloadable release assets.
data/docs/usage.md ADDED
@@ -0,0 +1,51 @@
1
+ # Using Vanken
2
+
3
+ ## Keyboard and terminal operation
4
+
5
+ The desktop and terminal views share the same commands. Use Tab and Shift+Tab to move between controls, Enter to activate, Escape to close dialogs, and arrow keys in lists. In the terminal and on Linux, use Ctrl for the shortcuts below; the native macOS window uses Command instead.
6
+
7
+ | Action | Shortcut |
8
+ | --- | --- |
9
+ | Open capture | Ctrl+O |
10
+ | Save as | Ctrl+Shift+S |
11
+ | Start or stop capture | Ctrl+E |
12
+ | Restart capture | Ctrl+Shift+R |
13
+ | Preferences | Ctrl+Shift+P |
14
+ | Command palette | Ctrl+Shift+K |
15
+ | Find packet / next / previous | Ctrl+F / Ctrl+N / Ctrl+B |
16
+ | Mark / ignore / time reference | Ctrl+M / Ctrl+D / Ctrl+T |
17
+ | Previous / next packet | Alt+Up / Alt+Down |
18
+ | Previous / next in conversation | Ctrl+, / Ctrl+. |
19
+ | Go to packet | Ctrl+G |
20
+ | Follow TCP Stream | Ctrl+Alt+Shift+T |
21
+ | Decode As | Ctrl+Shift+U |
22
+ | Reload plugins | Ctrl+Shift+L |
23
+ | Quit | Ctrl+Q |
24
+
25
+ Terminals differ in the keys they transmit. Use the command palette for a command whose shortcut is intercepted by the terminal. Type an action name and use Up/Down and Enter to run it. Column movement, width, and visibility have controls in the column editor, so they do not require dragging.
26
+
27
+ ## Profiles and settings
28
+
29
+ Preferences change the appearance, packet list, acquisition defaults, analysis limits, and optional name resolution. The language defaults to Japanese when `LANG` begins with `ja`, and English otherwise. Switching language rebuilds the current screen. High contrast is available from the theme menu or Preferences.
30
+
31
+ Create, duplicate, delete, and switch profiles from Preferences or the Profiles command. The default profile lives in the configuration directory; other profiles live under `profiles/NAME`. Each has `preferences.yml`, `columns.yml`, `coloring_rules.yml`, `filters.yml`, `decode_as.yml`, and `plugins.yml`. Configuration uses schema version 1, safe YAML, and atomic saves. Recent-file history and window geometry are shared outside profiles.
32
+
33
+ Stop acquisition before changing profiles or dissectors. Changes to analysis preferences during acquisition or file loading wait until it finishes. Reanalysis keeps the original bytes, marks, ignored packets, time references, and current selection, then refreshes summaries, details, and filters. Registered Ruby plugins execute with your account's permissions and require a trust confirmation before their first load.
34
+
35
+ ## Exporting and following streams
36
+
37
+ Export all analyzed packets, displayed packets, the selected packet, marked packets, the range between the first and last marks, or explicit ranges such as `1-10,15,20-`. An option excludes ignored packets. pcapng preserves interface metadata; pcap requires compatible link types. Ordinary Save includes all durable packets, including packets still awaiting analysis.
38
+
39
+ JSON follows redhound's packet schema; NDJSON writes one packet per line. CSV exports visible columns, including custom fields. Text exports the protocol tree. Outputs are written to a temporary file and replace the destination only after completion.
40
+
41
+ Follow TCP Stream removes duplicate retransmissions, handles sequence wrap and out-of-order segments, and displays missing-data markers. The display preview is limited to 16 MiB; saving reads the full reconstructed stream. Raw saving omits missing bytes rather than inventing data.
42
+
43
+ ## Limits and recovery
44
+
45
+ I/O graphs offer intervals of 0.01, 0.1, 1, 10, and 60 seconds. They show at most the latest 100,000 intervals and report omitted earlier intervals; choose a longer interval for long captures. Custom-column cells show at most 16 values and 4,096 bytes; the details and byte panes retain the full packet.
46
+
47
+ Acquisition can stop by packet count, duration, or byte count. Ring saving rotates pcapng files by size or time and replaces older slots after the configured file count. A size limit may be crossed by the packet that triggers rotation; packets are never split across files.
48
+
49
+ After an interrupted capture, the next start offers recovery or discard for owned, inactive session directories. Recovery reconstructs the analysis and keeps the capture unsaved until you save it. Sessions still used by a running process are excluded. A failed recovery preserves the raw session for another attempt.
50
+
51
+ See [capture permissions](../packaging/README.md) for administrator setup and [performance measurements](performance.md) for tested workloads and current limits.
@@ -4,11 +4,50 @@ require_relative "../errors"
4
4
  require_relative "../capture/launcher"
5
5
  require_relative "../capture/control_protocol"
6
6
  require_relative "../gateway/file_reader"
7
+ require_relative "../gateway/file_writer"
7
8
  require_relative "document"
8
9
 
9
10
  module Vanken
10
11
  module App
11
12
  class CaptureController
13
+ class RingReader
14
+ def initialize(reader, writer, on_error:)
15
+ @reader, @writer, @on_error = reader, writer, on_error
16
+ @flushed_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ end
18
+
19
+ def next_frame(timeout:)
20
+ frame = @reader.next_frame(timeout: timeout)
21
+ @writer << frame if frame
22
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ if !frame || now - @flushed_at >= 0.05
24
+ @writer.flush
25
+ @flushed_at = now
26
+ end
27
+ frame
28
+ rescue Vanken::FileError, IOError, SystemCallError
29
+ @on_error.call
30
+ raise
31
+ end
32
+
33
+ def eof? = @reader.eof?
34
+ def stop = @reader.stop
35
+ def close
36
+ return if @closed
37
+ @closed = true
38
+ begin
39
+ @writer.write_stats(@reader.stats)
40
+ ensure
41
+ begin
42
+ @writer.close
43
+ ensure
44
+ @reader.close
45
+ end
46
+ end
47
+ end
48
+ end
49
+ private_constant :RingReader
50
+
12
51
  class Error < Vanken::CaptureError
13
52
  attr_reader :code
14
53
 
@@ -20,6 +59,7 @@ module Vanken
20
59
 
21
60
  def initialize(launcher: nil, preferences: nil, on_update: nil, on_document: nil)
22
61
  @preferences, @on_update, @on_document = preferences, on_update, on_document
62
+ @custom_launcher = !launcher.nil?
23
63
  @launcher = launcher || Capture::Launcher.new(strategy: preferences&.get("capture.launcher") || :auto)
24
64
  @mutex = Mutex.new
25
65
  @state, @stats = :idle, {}
@@ -35,6 +75,14 @@ module Vanken
35
75
  def running? = @mutex.synchronize { !!@worker&.alive? }
36
76
  def snapshot = @mutex.synchronize { {state: @state, stats: @stats.dup, error: @error, warning: @warning, document: @document} }
37
77
 
78
+ def preferences=(value)
79
+ @mutex.synchronize do
80
+ raise Error, "cannot change capture preferences while capture is running" if @worker&.alive?
81
+ @preferences = value
82
+ @launcher = Capture::Launcher.new(strategy: value&.get("capture.launcher") || :auto) unless @custom_launcher
83
+ end
84
+ end
85
+
38
86
  def start(options)
39
87
  @mutex.synchronize do
40
88
  raise Error, "capture controller is closed" if @closed
@@ -89,7 +137,9 @@ module Vanken
89
137
  private
90
138
 
91
139
  def run
92
- handle = @launcher.launch(@options)
140
+ helper_options = @options.is_a?(Hash) ? @options.reject { |key, _| key.start_with?("ring_") } : @options
141
+ ring_writer = open_ring
142
+ handle = @launcher.launch(helper_options)
93
143
  @mutex.synchronize do
94
144
  @handle = handle
95
145
  stop_helper if @stop_requested
@@ -99,6 +149,7 @@ module Vanken
99
149
  @mutex.synchronize { @document = doc }
100
150
  @on_document&.call(doc)
101
151
  reader = Gateway::FileReader.new(handle.stdout)
152
+ reader = RingReader.new(reader, ring_writer, on_error: -> { stop }) if ring_writer
102
153
  doc.ingest(reader, live: true)
103
154
  doc.wait
104
155
  unless control.join(3)
@@ -122,6 +173,7 @@ module Vanken
122
173
  control&.join
123
174
  doc&.wait
124
175
  reader&.close unless doc
176
+ ring_writer&.close
125
177
  handle&.close
126
178
  @mutex.synchronize do
127
179
  @handle = nil
@@ -130,6 +182,19 @@ module Vanken
130
182
  notify
131
183
  end
132
184
 
185
+ def open_ring
186
+ return unless @options.is_a?(Hash)
187
+ path = @options["ring_path"].to_s
188
+ return if path.empty?
189
+
190
+ values = %w[max_bytes interval file_count].to_h do |key|
191
+ value = @options["ring_#{key}"]
192
+ [key.to_sym, value && value != 0 ? value : nil]
193
+ end.compact
194
+ raise Error, "ring saving requires a size or time rotation condition" unless values[:max_bytes] || values[:interval]
195
+ Gateway::FileWriter.open(File.expand_path(path), format: :pcapng, **values)
196
+ end
197
+
133
198
  def consume_control(io)
134
199
  while (line = io.gets(Capture::ControlProtocol::MAX_LINE_BYTES + 1))
135
200
  event = Capture::ControlProtocol.parse(line)
@@ -9,27 +9,36 @@ require_relative "../capture/receiver"
9
9
  require_relative "../capture/analyzer"
10
10
  require_relative "../capture/analyzer_process"
11
11
  require_relative "document_jobs"
12
+ require_relative "../config/analysis_settings"
13
+ require_relative "document_analysis"
14
+ require_relative "navigation"
15
+ require_relative "../config/columns"
12
16
 
13
17
  module Vanken
14
18
  module App
15
19
  class Document
16
20
  include DocumentJobs
21
+ include DocumentAnalysis
22
+ include Navigation
17
23
  attr_reader :store, :columns, :annotations, :error, :path, :source, :progress, :filter,
18
- :marked, :ignored, :time_references, :catalog, :analysis_options
24
+ :marked, :ignored, :time_references, :catalog, :analysis_options, :analysis_gateway_options
19
25
  attr_accessor :frame_latency, :scanner
20
26
 
21
- def initialize(on_update: nil, preferences: nil, store: nil, scanner: nil, process_analysis: true)
27
+ def initialize(on_update: nil, preferences: nil, store: nil, scanner: nil, process_analysis: true, decode_as: nil, plugins: nil)
28
+ @verify_checksums = preferences&.get("analysis.verify_checksums") || false
29
+ settings = preferences && Config::AnalysisSettings.new(directory: preferences.directory)
30
+ @analysis_gateway_options = {verify_checksums: @verify_checksums, decode_as: decode_as || settings&.decode_as || [], plugins: plugins || settings&.plugins || []}.freeze
31
+ @dissector = Gateway::Dissector.new(**@analysis_gateway_options)
22
32
  @store = store || Core::FrameStore.new
23
33
  @columns = Core::ColumnStore.new
24
34
  @annotations = Core::AnnotationStore.new(@store.directory)
25
35
  @cache = Core::RowCache.new(limit: preferences&.get("packet_list.row_cache_rows") || 20_000)
26
- @verify_checksums = preferences&.get("analysis.verify_checksums") || false
27
- @dissector = Gateway::Dissector.new(verify_checksums: @verify_checksums)
28
36
  @process_analysis = process_analysis
29
37
  @analysis_options = {max_state_bytes: (preferences&.get("analysis.max_state_mib") || 256) << 20,
30
38
  max_flows: preferences&.get("analysis.max_flows") || 100_000}.freeze
31
- @catalog = Gateway::FieldCatalog.new
39
+ @catalog = Gateway::FieldCatalog.new(registry: @dissector.registry)
32
40
  @mutex, @condition = Mutex.new, ConditionVariable.new
41
+ @close_mutex = Mutex.new
33
42
  @jobs, @count, @generation = [], 0, 0
34
43
  @display = nil
35
44
  @marked, @ignored, @time_references = Set.new, Set.new, Set.new
@@ -38,15 +47,7 @@ module Vanken
38
47
  @scanner = scanner
39
48
  @scan_concurrency = preferences&.get("analysis.workers") || 4
40
49
  @filter_tasks = []
41
- @details = File.open(File.join(@store.directory, "details.jsonl"), "w+b", 0o600)
42
- @detail_reader = File.open(File.join(@store.directory, "details.jsonl"), "rb")
43
- @detail_index = File.open(File.join(@store.directory, "reassembled.idx"), "w+b", 0o600)
44
- @detail_offsets = {}
45
- @summaries = File.open(File.join(@store.directory, "summaries.bin"), "w+b", 0o600)
46
- @summary_index = File.open(File.join(@store.directory, "summaries.idx"), "w+b", 0o600)
47
- @summary_reader = File.open(File.join(@store.directory, "summaries.bin"), "rb")
48
- @summary_index_reader = File.open(File.join(@store.directory, "summaries.idx"), "rb")
49
- @summary_count = 0
50
+ open_analysis_files
50
51
  @frame_latency = 0.0
51
52
  end
52
53
 
@@ -75,12 +76,18 @@ module Vanken
75
76
  def displayed_count = @mutex.synchronize { @display ? @display.size : @count }
76
77
  def number_at(index) = @mutex.synchronize { @display ? @display.fetch(index) : (index + 1).tap { |number| raise IndexError unless number.between?(1, @count) } }
77
78
  def display_numbers = @mutex.synchronize { @display ? @display.dup : (1..@count).to_a }
78
- def dirty? = @dirty
79
+ def dirty? = @dirty || (@source == :live && @store.count > (@saved_count || 0))
79
80
  def loading? = !@analyzed && !@cancelled
80
81
  def cancelled? = @cancelled
81
82
  def received? = @received
82
83
  def complete? = @analyzed
83
84
  def closing? = !!@closing
85
+ def analysis_stopped? = closing? || !!@reanalysis_cancelled
86
+ def capture_stats
87
+ value = @reader.respond_to?(:stats) && @reader.stats
88
+ value.respond_to?(:to_h) ? value.to_h : {}
89
+ end
90
+ def expert_summary = @mutex.synchronize { {count: @annotations.expert_count, severity: @annotations.expert_max} }
84
91
  def stream_frames(stream) = @mutex.synchronize { @annotations.streams[stream].dup }
85
92
  def packet(number) = @dissector.dissect(@store.read(number))
86
93
  # @rbs (Integer number) -> Gateway::PacketSnapshot?
@@ -93,7 +100,33 @@ module Vanken
93
100
  base = @mutex.synchronize { @columns[number] }
94
101
  frame = @store.metadata(number)
95
102
  info = @cache.fetch(number) { persisted_summary(number) || persisted_details(number)&.fetch("info", nil) || packet(number).info }
96
- base.merge(no: number, number: number, timestamp_ns: frame[:timestamp_ns], length: frame[:original_length], info: info)
103
+ base.merge(no: number, number: number, timestamp_ns: frame[:timestamp_ns], length: frame[:original_length], info: info).merge(custom_row(number))
104
+ end
105
+ def custom_columns = @custom_columns || []
106
+ def custom_columns=(columns)
107
+ raise Vanken::ConfigError, "invalid custom columns" unless columns.is_a?(Array) && columns.size <= 64 && columns.all? do |item|
108
+ item.is_a?(Hash) && Config::Columns.valid_field?(item["field"]) && item["key"] == "field:#{item['field']}" && [true, false].include?(item["visible"])
109
+ end
110
+ saved = columns.map { |item| item.slice("key", "field", "visible").transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze }.freeze
111
+ @mutex.synchronize do
112
+ unless saved == @custom_columns
113
+ @custom_columns = saved
114
+ @sort_generation = (@sort_generation || 0) + 1
115
+ end
116
+ end
117
+ end
118
+ def custom_row(number)
119
+ fields = custom_columns.select { |item| item["visible"] }
120
+ return {} if fields.empty?
121
+ packet = view(number)
122
+ resolver = Core::DisplayFilter::FieldResolver.new(@catalog)
123
+ fields.to_h do |item|
124
+ field = item.fetch("field")
125
+ [item.fetch("key").to_sym, Config::Columns.display(packet.values(field), type: resolver.resolve(field).type)]
126
+ end
127
+ end
128
+ def custom_sort_value(number, field)
129
+ Config::Columns.sort_value(view(number).values(field).first, type: Core::DisplayFilter::FieldResolver.new(@catalog).resolve(field).type)
97
130
  end
98
131
  def details(number)
99
132
  saved = persisted_details(number)
@@ -112,7 +145,7 @@ module Vanken
112
145
  program, generation, snapshot = @mutex.synchronize { [@filter, @generation, @filter_context] }
113
146
  matched = !program || program.match?(view(number, packet: packet, snapshot: snapshot))
114
147
  committed = @mutex.synchronize do
115
- next false if generation != @generation
148
+ next false if generation != @generation || !snapshot.equal?(@filter_context)
116
149
  committing = true
117
150
  @columns.append(columns)
118
151
  @annotations.append(number, annotation)
@@ -123,8 +156,8 @@ module Vanken
123
156
  @detail_index.write([number, @detail_offsets[number]].pack("Q<Q<"))
124
157
  @detail_index.flush
125
158
  end
159
+ record_live_predecessors(number, number, matched ? [number] : [], context: snapshot)
126
160
  @count = number
127
- @dirty = true if @source == :live
128
161
  @display << number if @display && matched
129
162
  true
130
163
  end
@@ -142,6 +175,7 @@ module Vanken
142
175
  @columns.append(source: "", destination: "", protocol: "Malformed", src_port: -1, dst_port: -1, ip_proto: -1, layers: [])
143
176
  @annotations.append(number, tcp_stream: -1, seq_rel: -1, ack_rel: -1, analysis_flags: [], expert_max: 3,
144
177
  expert_items: [{severity: :error, code: "vanken.internal_error", protocol: "vanken", message: error.message}], extra: {})
178
+ record_live_predecessors(number, number, [])
145
179
  @count = number
146
180
  @display << number if @display && !@filter
147
181
  end
@@ -149,14 +183,20 @@ module Vanken
149
183
  end
150
184
  # Only immutable context for this unpublished range crosses the worker pipe.
151
185
  def analysis_configuration(first, last)
186
+ raise Vanken::Error, "invalid analysis range" unless first.is_a?(Integer) && last.is_a?(Integer) && first.positive? && (last - first + 1).between?(1, 256)
152
187
  @mutex.synchronize do
153
188
  historical = !!@filter_context
154
189
  current = @filter_context || {marked: @marked, ignored: @ignored, references: @time_references,
155
190
  predecessors: nil, limit: @count, last_displayed: @display ? @display.last : @count}
156
- snapshot = current.merge(marked: current[:marked].select { |n| n.between?(first, last) }.to_set,
191
+ predecessors = if current[:live_predecessors]
192
+ (first..last).to_h { |number| [number, displayed_predecessor(number, current)] }
193
+ else
194
+ current[:predecessors]&.slice(*(first..last).to_a)
195
+ end
196
+ snapshot = current.except(:live_predecessors, :live_predecessor_start).merge(marked: current[:marked].select { |n| n.between?(first, last) }.to_set,
157
197
  ignored: current[:ignored].select { |n| n.between?(first, last) }.to_set,
158
- references: current[:references].dup, predecessors: nil,
159
- unfiltered_predecessor: historical && current[:predecessors].nil?)
198
+ references: current[:references].dup, predecessors: predecessors,
199
+ unfiltered_predecessor: historical && current[:predecessors].nil? && !current[:live_predecessors])
160
200
  {expression: @filter&.expression || "", generation: @generation, snapshot: snapshot,
161
201
  historical_context: historical, context_token: @filter_context&.object_id}
162
202
  end
@@ -185,8 +225,8 @@ module Vanken
185
225
  @summary_index.write(offsets)
186
226
  [@annotations, @details, @detail_index, @summaries, @summary_index].each(&:flush)
187
227
  @catalog.merge(batch[:fields])
228
+ record_live_predecessors(first, last, batch[:matches])
188
229
  @summary_count = @count = last
189
- @dirty = true if @source == :live
190
230
  @display.concat(batch[:matches]) if @display
191
231
  true
192
232
  end
@@ -199,7 +239,18 @@ module Vanken
199
239
  notify
200
240
  end
201
241
  def receiving_done = (@received = true; signal)
202
- def analyzing_done = (@annotations.flush; @analyzed = true; notify(force: true))
242
+ def analyzing_done
243
+ @annotations.flush
244
+ @mutex.synchronize do
245
+ @filter_context = nil if @reanalysis_context && @filter_context.equal?(@reanalysis_context)
246
+ @reanalysis_context = nil
247
+ @rebuilding_filter_basis = nil
248
+ @rebuilding = false
249
+ @reanalysis_cancelled = false
250
+ @analyzed = true
251
+ end
252
+ notify(force: true)
253
+ end
203
254
  def signal = @mutex.synchronize { @condition.broadcast }
204
255
  def wait_for_frames = @mutex.synchronize { @condition.wait(@mutex, 0.05) }
205
256
  def fail(error) = (@error = error; notify(force: true))
@@ -227,6 +278,15 @@ module Vanken
227
278
  signal
228
279
  self
229
280
  end
281
+ def cancel_reanalysis
282
+ @mutex.synchronize do
283
+ if @rebuilding
284
+ @reanalysis_cancelled = true
285
+ @condition.broadcast
286
+ end
287
+ end
288
+ self
289
+ end
230
290
  def wait(timeout = nil)
231
291
  deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
232
292
  @jobs.each do |job|
@@ -237,22 +297,25 @@ module Vanken
237
297
  self
238
298
  end
239
299
  def close
240
- return self if @closed
241
- @closing = true
242
- cancel
243
- cancel_scan
244
- wait(3)
245
- @annotations.close
246
- @details.close
247
- @detail_reader.close
248
- @detail_index.close
249
- @summaries.close
250
- @summary_index.close
251
- @summary_reader.close
252
- @summary_index_reader.close
253
- @store.close
254
- @closed = true
255
- self
300
+ @close_mutex.synchronize do
301
+ return self if @closed
302
+ @closing = true
303
+ cancel_search
304
+ cancel
305
+ cancel_scan
306
+ wait(3)
307
+ @annotations.close
308
+ @details.close
309
+ @detail_reader.close
310
+ @detail_index.close
311
+ @summaries.close
312
+ @summary_index.close
313
+ @summary_reader.close
314
+ @summary_index_reader.close
315
+ @store.close
316
+ @closed = true
317
+ self
318
+ end
256
319
  end
257
320
 
258
321
  private