vanken 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ff6f270de34fbee27bb93839749a5105ece2667ef59e3281c28e37d5dee782a
4
- data.tar.gz: cfb0118a6e6c0b1b6a026ca8254e58b2e20f260b11e75bedbbce21a6aa47755b
3
+ metadata.gz: 9db141f2b218a50e8cb5d228b14b35d6ff728e3fc48fbd6abddf18e56f903284
4
+ data.tar.gz: 65a40d15a931d59b2e618aab1cfbc9b8fd97bea13d0593b8b1938d611ba5bad2
5
5
  SHA512:
6
- metadata.gz: d7913b548c7ce8fd8bb0531542b70d633c1c06525bb2be762230661ec2d2691a233c81116376ab3c6fa0857db76c8796c4e585271431fd6025af7c130fc55fab
7
- data.tar.gz: 1de08c7960be93a43239e88564f3267622406597909a7bd18d7ca59f5bf77dab29526da4b2852405cf711a2066342bc332bad70121ad9f67bead285eb9b6812b
6
+ metadata.gz: 5b7de2090ab6735713dcb9855d67103339b2dfe75a94b51486b9d0bcb797e0b3c8286fcf3b531f50ea8375eaf732721d2124ffdcb32fd0940a69f03b6893c4ab
7
+ data.tar.gz: 3f4600b9db5305b86b35fa821f6dee78bdd323458b50359043f4e252481ba16d6414bfbfd8c8a22e8cec1566280d7933dbbc0df8612015ce4ea07b277a4ece43
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add `vanken-setup-permissions` to install Linux capture wrappers and polkit or sudoers policies, and configure macOS BPF access through a LaunchDaemon. Installation validates privileged runtime and destination ownership before granting access.
6
+ - Include the Linux desktop entry, capture policies, fixed-path wrappers, and macOS setup files in the gem and downloadable GitHub release assets.
7
+
8
+ 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).
9
+
10
+ Live acquisition and redraw latency on slower systems retain the [documented performance limits](https://github.com/ydah/vanken/blob/main/docs/performance.md).
11
+
3
12
  ## 0.2.0
4
13
 
5
14
  - Add editable packet coloring, four search modes, marks, ignored packets, time references, and conversation navigation.
data/README.md CHANGED
@@ -67,6 +67,8 @@ bundle exec exe/vanken-capture --check --interface lo
67
67
 
68
68
  Direct acquisition works when the account already has permission. Privileged launching requires a root-owned installation with a fixed interpreter and dependencies; see [capture helper setup](packaging/README.md). Run the desktop application as a regular user.
69
69
 
70
+ The gem includes `vanken-setup-permissions` for administrator installation of Linux capture permissions and the macOS BPF LaunchDaemon. Review the platform-specific setup guide and the command's `--dry-run` output first. GitHub releases also include the permission policies, desktop entry, wrappers, and macOS script in a packaging archive.
71
+
70
72
  ## Command line
71
73
 
72
74
  ```sh
data/docs/performance.md CHANGED
@@ -121,3 +121,46 @@ On 2026-10-02, Ruby 4.0.6 with YJIT on arm64 Darwin compared the same 20,000 com
121
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
122
 
123
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.
124
+
125
+ ## Release 0.2 and 0.3 verification with public Zaniah 0.12.4
126
+
127
+ The [final nightly run](https://github.com/ydah/vanken/actions/runs/36999466662) tested commit `6d05e2a` with Ruby 3.4.10, YJIT, public redhound 2.0.0.rc2, and public Zaniah 0.12.4 on the shared x86_64 Linux runner. The receiver, complete one-million-packet analysis, all filters, graph accounting, and 5,000 deterministic fuzz cases passed their functional checks. Commit `2169b6a` adds the administrator installer and packaging without changing this analysis or rendering code.
128
+
129
+ | Measurement | Result | Target |
130
+ | --- | ---: | ---: |
131
+ | Receiver, 1,000,000 frames | 181,777 frames/s | ≥ 100,000 |
132
+ | Ingest and analyze, 1,000,000 frames | 4,073 frames/s; 245.5186 s | ≥ 15,000; missed |
133
+ | Parent / combined / after-filter RSS increment | 80.26 / 87.48 / 91.06 B per frame | ≤ 200 |
134
+ | Fast / four-worker slow filter | 1.9499 / 22.9614 s | ≤ 3 / 60 s |
135
+ | Four-worker address/port cBPF filter | 7.1322 s; 1,000,000 matches | Reported separately |
136
+ | Static full application, total / scene render p95 | 73.154 / 71.903 ms | Scene ≤ 33 ms; missed |
137
+ | Growing full application, total / scene render p95 | 44.370 / 42.490 ms | Scene ≤ 33 ms; missed |
138
+
139
+ The growing full-application source produced only 427.6 frames/s during sampling, so that measurement does not establish responsiveness at 5,000 frames/s. The isolated virtual-table ingestion measurement similarly does not replace the complete application or live capture. Job success does not mean all numeric targets passed.
140
+
141
+ The same nightly run compared VDF and cBPF over the same 20,000 packets, with three samples per evaluator. Median times were 1.239493 s and 0.055858 s respectively, a 22.19-fold improvement, with identical match counts. Two-series I/O graph rebuilds for 200,000 frames took 1.024516, 1.011629, 1.005382, 0.996721, and 0.996433 s at intervals 0.01, 0.1, 1, 10, and 60 s. The three shortest intervals narrowly exceeded the one-second target.
142
+
143
+ On the Mac with Ruby 4.0.6, YJIT, and public Zaniah 0.12.4, the final 100,000,032-byte file check displayed its first verified row in 645.209 ms. Twenty distinct selections had p50 26.660 ms, p95 35.778 ms, and maximum 51.223 ms. The first-row target passed; the every-selection 50 ms target failed on the first selection. These are single-run headless production-window measurements with the same exclusions described above.
144
+
145
+ A native Mac run with 120 samples verified opening, selection, filtering, and all 25,256 captured frames. Scene p95 was 28.804 ms and total native tick p95 was 47.850 ms. During sampling, the source delivered 19,453 frames in 4.40697 s, or 4,414.14 frames/s. The scene result meets 33 ms at that observed rate, but does not establish the requested 5,000 frames/s target.
146
+
147
+ The [final five-minute Linux capture run](https://github.com/ydah/vanken/actions/runs/36999897569), at `2169b6a`, used Ruby 3.4.11 with YJIT and the public dependencies above. The actual administrator command installed 74 root-owned files, and capture-boundary tests verified the installation before running the UI with UID/EUID 1000. A real PTY additionally passed file opening, packet selection, filtering, the command palette, capture options, actual capture start/stop, and quit.
148
+
149
+ The independent sender delivered 1,500,000 frames over 300.000066 s at 4,999.9989 frames/s. Kernel received, helper captured, durable, and final analyzed counts all equaled 1,500,000; drops, interface drops, and freezes were zero. Analysis drained 208.956 s after traffic ended, with a maximum sampled backlog of 578,656 frames. This establishes capture integrity and sender pacing while retaining the analysis-throughput limitation.
150
+
151
+ During actual traffic, 2,492 frames rendered and 2,395 showed analyzed-row growth. Scene p95 was 78.807 ms and total render p95 was 81.711 ms, exceeding the 33 ms scene target. The file check in the same run displayed its first row in 788.282 ms, passing one second; selection p95 was 104.404 ms and maximum 111.676 ms, exceeding 50 ms. The earlier measurements remain above for comparison; shared-runner results do not establish a controlled before/after speedup.
152
+
153
+ ### Same-host analysis regression comparison
154
+
155
+ The old Linux analysis time of 206.2345 s and the latest 245.5186 s came from different shared VMs. A separate same-host comparison checks the release procedure's 15% regression threshold without treating those environments as identical.
156
+
157
+ The initial `26b3e7b` source and current `2169b6a` source were archived and run alternately on the same Mac. Both used the current public dependency lockfile, Ruby 4.0.6 with YJIT, a fresh Ruby process and analyzer child, the identical 58-byte UDP fixture and Enumerator, and a 50-frame warm-up. Each timed `Document.ingest(...).wait` with `process_analysis: true` over 100,000 frames. Initialization, warm-up, cleanup, kernel acquisition, and UI were excluded; no other local tests ran concurrently.
158
+
159
+ | Sample | Initial source, seconds | Current source, seconds |
160
+ | --- | ---: | ---: |
161
+ | 1 | 11.307215 | 11.508561 |
162
+ | 2 | 11.623641 | 12.657844 |
163
+ | 3 | 12.123842 | 12.085011 |
164
+ | Median | 11.623641 | 12.085011 |
165
+
166
+ Median elapsed time increased 3.97%; median throughput changed from 8,603.16 to 8,274.71 frames/s, a 3.82% decrease. The 15% threshold was not exceeded for this workload. All six runs verified exactly 100,000 durable and analyzed frames, UDP decoding, no document error, and analyzer-child cleanup. This comparison does not explain the separate shared-VM Linux timing difference or establish the 15,000 frames/s target.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vanken/capture/permission_setup"
5
+
6
+ exit Vanken::Capture::PermissionSetup.run(ARGV)
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "json"
5
+ require "etc"
6
+ require "shellwords"
7
+ require "tempfile"
8
+ require "open3"
9
+
10
+ module Vanken
11
+ module Capture
12
+ class PermissionSetup
13
+ PREFIX = "/usr/local/libexec/vanken"
14
+ SOURCE = File.expand_path("../../..", __dir__)
15
+
16
+ def self.run(argv, stdout: $stdout, stderr: $stderr)
17
+ options = {}
18
+ dry_run = false
19
+ parser = OptionParser.new do |flags|
20
+ flags.banner = "Usage: vanken-setup-permissions [--dry-run] [--ruby PATH --gem-home PATH] [--policy polkit|sudoers] [--group GROUP]"
21
+ flags.on("--platform VALUE", %w[linux darwin]) { |value| options[:platform] = value.to_sym }
22
+ flags.on("--ruby PATH") { |value| options[:ruby] = value }
23
+ flags.on("--gem-home PATH") { |value| options[:gem_home] = value }
24
+ flags.on("--policy VALUE", %w[polkit sudoers]) { |value| options[:policy] = value.to_sym }
25
+ flags.on("--group GROUP") { |value| options[:group] = value }
26
+ flags.on("--root DIRECTORY", "Stage files without activating system permissions") { |value| options[:root] = value }
27
+ flags.on("--dry-run") { dry_run = true }
28
+ flags.on("-h", "--help") { stdout.puts(flags); return 0 }
29
+ end
30
+ known = %w[--platform --ruby --gem-home --policy --group --root --dry-run --help]
31
+ argv.each do |value|
32
+ raise ArgumentError, "unknown option: #{value}" if value.start_with?("--") && !known.include?(value.split("=", 2).first)
33
+ end
34
+ remaining = parser.parse(argv.dup)
35
+ raise ArgumentError, "unexpected arguments: #{remaining.join(' ')}" unless remaining.empty?
36
+ setup = new(**options)
37
+ setup.install unless dry_run
38
+ stdout.puts(JSON.pretty_generate(platform: setup.platform, dry_run: dry_run, staged: setup.staged?,
39
+ files: setup.plan.keys, next_steps: setup.next_steps))
40
+ 0
41
+ rescue ArgumentError, OptionParser::ParseError => error
42
+ stderr.puts(error.message)
43
+ 2
44
+ rescue SystemCallError, IOError => error
45
+ stderr.puts(error.message)
46
+ 1
47
+ end
48
+
49
+ attr_reader :platform
50
+
51
+ def initialize(platform: nil,
52
+ ruby: nil, gem_home: nil, policy: :polkit, group: "vanken", root: "/")
53
+ platform ||= RUBY_PLATFORM.include?("darwin") ? :darwin : (RUBY_PLATFORM.include?("linux") ? :linux : :unsupported)
54
+ @platform, @ruby, @gem_home, @policy, @group = platform.to_sym, ruby, gem_home, policy.to_sym, group
55
+ raise ArgumentError, "unsupported platform" unless %i[linux darwin].include?(@platform)
56
+ raise ArgumentError, "invalid policy" unless %i[polkit sudoers].include?(@policy)
57
+ raise ArgumentError, "invalid capture group" unless @group.match?(/\A[a-z_][a-z0-9_-]{0,31}\z/)
58
+ if @platform == :linux
59
+ [@ruby, @gem_home].each do |path|
60
+ raise ArgumentError, "Linux setup requires absolute --ruby and --gem-home paths" unless path&.start_with?("/") && !path.include?("\n")
61
+ end
62
+ end
63
+ raise ArgumentError, "staging root must be an existing absolute directory" unless root.start_with?("/") && File.directory?(root) && !File.symlink?(root)
64
+ @root = File.realpath(root)
65
+ end
66
+
67
+ def staged? = @root != "/"
68
+
69
+ def plan
70
+ if @platform == :darwin
71
+ return {"#{PREFIX}/chmod-bpf" => asset("macos/chmod-bpf", mode: 0o755),
72
+ "/Library/LaunchDaemons/org.vanken.chmod-bpf.plist" => asset("macos/org.vanken.chmod-bpf.plist")}
73
+ end
74
+ files = {"#{PREFIX}/vanken-capture" => asset("linux/vanken-capture.wrapper", mode: 0o755),
75
+ "#{PREFIX}/helper" => {contents: File.binread(File.join(SOURCE, "exe/vanken-capture")), mode: 0o755},
76
+ "/usr/share/applications/vanken.desktop" => asset("linux/vanken.desktop")}
77
+ if @policy == :polkit
78
+ files["/usr/share/polkit-1/actions/org.vanken.capture.policy"] = asset("linux/org.vanken.capture.policy")
79
+ else
80
+ files["/etc/sudoers.d/vanken"] = asset("linux/vanken.sudoers", mode: 0o440)
81
+ end
82
+ Dir.glob(File.join(SOURCE, "lib", "**", "*"), File::FNM_DOTMATCH).each do |path|
83
+ raise ArgumentError, "source library contains a symlink" if File.symlink?(path)
84
+ next if File.directory?(path)
85
+ files["#{PREFIX}/#{path.delete_prefix("#{SOURCE}/")}"] = {contents: File.binread(path), mode: 0o644}
86
+ end
87
+ files
88
+ end
89
+
90
+ def install
91
+ raise ArgumentError, "system installation requires root; use --dry-run or --root for review" unless staged? || Process.euid.zero?
92
+ raise ArgumentError, "use --root or --dry-run to prepare files for another platform" unless staged? || RUBY_PLATFORM.include?(@platform.to_s)
93
+ validate_runtime! if @platform == :linux && !staged?
94
+ validate_group! if @platform == :darwin || @policy == :sudoers
95
+ plan.each do |absolute, item|
96
+ destination = File.join(@root, absolute.delete_prefix("/"))
97
+ secure_directory!(File.dirname(destination))
98
+ safe_entry!(destination) if File.exist?(destination) || File.symlink?(destination)
99
+ Tempfile.create([".vanken-", ".tmp"], File.dirname(destination)) do |file|
100
+ file.binmode
101
+ file.write(item[:contents])
102
+ file.chmod(item[:mode])
103
+ file.flush
104
+ validate_sudoers!(file.path) if absolute == "/etc/sudoers.d/vanken" && !staged?
105
+ File.rename(file.path, destination)
106
+ end
107
+ end
108
+ self
109
+ end
110
+
111
+ def validate_runtime!
112
+ trusted_tree!(@ruby)
113
+ raise ArgumentError, "Ruby interpreter must be executable" unless File.file?(@ruby) && File.executable?(@ruby)
114
+ runtime_prefix = File.dirname(File.dirname(File.realpath(@ruby)))
115
+ trusted_tree!(runtime_prefix) unless ["/", "/usr"].include?(runtime_prefix)
116
+ paths, status = Open3.capture2({"PATH" => "/usr/bin:/bin"}, @ruby, "--disable-gems", "-e", "$LOAD_PATH.each { |path| puts path }", unsetenv_others: true, chdir: "/")
117
+ raise ArgumentError, "could not inspect the Ruby runtime" unless status.success?
118
+ paths.each_line { |path| trusted_tree!(path.strip) }
119
+ trusted_tree!(@gem_home)
120
+ raise ArgumentError, "gem home must be a directory" unless File.directory?(@gem_home)
121
+ trusted_tree!(SOURCE)
122
+ self
123
+ end
124
+
125
+ def validate_group!
126
+ Etc.getgrnam(@group)
127
+ rescue ArgumentError
128
+ raise ArgumentError, "create the #{@group} capture group and add the intended users before installing"
129
+ end
130
+
131
+ def next_steps
132
+ if @platform == :darwin
133
+ ["sudo launchctl bootstrap system /Library/LaunchDaemons/org.vanken.chmod-bpf.plist",
134
+ "Log out and log in after changing group membership, then run vanken-capture --check --interface lo0."]
135
+ else
136
+ ["Run vanken-capture --check --interface IF as a normal user, then start Vanken."]
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def asset(relative, mode: 0o644)
143
+ contents = File.binread(File.join(SOURCE, "packaging", relative)).gsub("VANKEN_GROUP", @group)
144
+ contents = contents.gsub("VANKEN_RUBY", Shellwords.escape(@ruby.to_s)).gsub("VANKEN_GEM_HOME", Shellwords.escape(@gem_home.to_s))
145
+ {contents: contents, mode: mode}
146
+ end
147
+
148
+ def trusted_tree!(path, visited = {})
149
+ resolved = trusted_path!(path)
150
+ return if visited[resolved]
151
+ visited[resolved] = true
152
+ return unless File.directory?(resolved)
153
+ Dir.children(resolved).each { |name| trusted_tree!(File.join(resolved, name), visited) }
154
+ rescue SystemCallError => error
155
+ raise ArgumentError, "cannot verify root-owned privileged path: #{error.message}"
156
+ end
157
+
158
+ def trusted_path!(path)
159
+ resolved = File.realpath(path)
160
+ current = path
161
+ loop do
162
+ stat = File.lstat(current)
163
+ unless stat.uid.zero? && (stat.symlink? || (stat.mode & 0o022).zero?) && (stat.file? || stat.directory? || stat.symlink?)
164
+ raise ArgumentError, "privileged paths must be root-owned without group or other write access"
165
+ end
166
+ if stat.symlink?
167
+ target = File.readlink(current)
168
+ trusted_path!(target.start_with?("/") ? target : File.join(File.dirname(current), target))
169
+ end
170
+ break if current == "/"
171
+ current = File.dirname(current)
172
+ end
173
+ trusted_path!(resolved) if path != resolved
174
+ resolved
175
+ end
176
+
177
+ def safe_entry!(path)
178
+ stat = File.lstat(path)
179
+ raise ArgumentError, "unsafe installation path or symlink: #{path}" unless stat.uid == Process.euid && (stat.mode & 0o022).zero? && (stat.file? || stat.directory?)
180
+ end
181
+
182
+ def secure_directory!(path)
183
+ safe_entry!(@root)
184
+ current = @root
185
+ path.delete_prefix(@root).split("/").reject(&:empty?).each do |part|
186
+ current = File.join(current, part)
187
+ Dir.mkdir(current, 0o755) unless File.exist?(current) || File.symlink?(current)
188
+ safe_entry!(current)
189
+ raise ArgumentError, "installation parent is not a directory" unless File.directory?(current)
190
+ end
191
+ end
192
+
193
+ def validate_sudoers!(path)
194
+ executable = %w[/usr/sbin/visudo /sbin/visudo].find { |candidate| File.executable?(candidate) }
195
+ raise ArgumentError, "visudo is required to install sudoers permissions" unless executable
196
+ _, errors, status = Open3.capture3(executable, "-cf", path)
197
+ raise ArgumentError, "invalid sudoers configuration: #{errors}" unless status.success?
198
+ end
199
+ end
200
+ end
201
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vanken
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/packaging/README.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Live capture permissions
2
2
 
3
+ `vanken-setup-permissions` installs the files shipped in `packaging/linux/` or `packaging/macos/`. Review the macOS plan first:
4
+
5
+ ```sh
6
+ vanken-setup-permissions --platform darwin --dry-run
7
+ ```
8
+
9
+ On Linux, provide the canonical absolute paths of a root-owned Ruby 3.3 or newer runtime and its isolated gem home:
10
+
11
+ ```sh
12
+ vanken-setup-permissions --platform linux --dry-run \
13
+ --ruby /opt/vanken-ruby/bin/ruby --gem-home /opt/vanken-ruby/gems
14
+ ```
15
+
16
+ The following administrator installation assumes Ruby is already installed under `/opt/vanken-ruby`, with root-owned runtime files that are not writable by group or other users. Install Vanken into the isolated root-owned gem home, then run its packaged setup command with that same interpreter:
17
+
18
+ ```sh
19
+ sudo /usr/bin/env -i PATH=/usr/bin:/bin GEM_HOME=/opt/vanken-ruby/gems GEM_PATH=/opt/vanken-ruby/gems \
20
+ /opt/vanken-ruby/bin/ruby /opt/vanken-ruby/bin/gem install vanken --version 0.3.0 --no-document
21
+ sudo /usr/bin/env -i PATH=/usr/bin:/bin GEM_HOME=/opt/vanken-ruby/gems GEM_PATH=/opt/vanken-ruby/gems \
22
+ /opt/vanken-ruby/bin/ruby /opt/vanken-ruby/gems/bin/vanken-setup-permissions \
23
+ --ruby /opt/vanken-ruby/bin/ruby --gem-home /opt/vanken-ruby/gems --policy polkit
24
+ ```
25
+
26
+ The installer checks the interpreter, its library paths, runtime tree, gem home, and source tree for root ownership and writable paths before copying anything. Root-owned runtime library aliases are allowed only when every link, target, and parent path passes the same checks. The wrapper clears all inherited environment variables, fixes the interpreter and gem paths, and runs only `/usr/local/libexec/vanken/helper`. The polkit policy authorizes that exact wrapper path. Launch Vanken as your ordinary user; polkit provides the administrator authentication dialog when capture requires elevation.
27
+
28
+ For headless Linux or sudo instead of polkit, create a dedicated capture group and add only users who should inspect network traffic. Log out and log in after changing group membership, then use `--policy sudoers --group vanken` in the setup command. The installer requires `visudo` and validates the rule before installing `/etc/sudoers.d/vanken`. It never grants capabilities to a shared Ruby interpreter.
29
+
30
+ On macOS, create the intended group, install the BPF daemon, and activate it:
31
+
32
+ ```sh
33
+ sudo dseditgroup -o create vanken
34
+ sudo dseditgroup -o edit -a "$USER" -t user vanken
35
+ sudo "$(command -v vanken-setup-permissions)" --group vanken
36
+ sudo launchctl bootstrap system /Library/LaunchDaemons/org.vanken.chmod-bpf.plist
37
+ ```
38
+
39
+ Log out and log in after joining the group. The daemon grants the group mode `0660` on existing BPF devices at startup and every ten seconds, including devices added later. It runs the fixed root-owned shell script; Vanken and the capture helper run as the normal user. Verify access with `vanken-capture --check --interface lo0`. If Wireshark's ChmodBPF already manages these devices, use that configuration instead of installing a second daemon.
40
+
41
+ Use `--root /absolute/staging-directory` to prepare files in an existing directory without activating system permissions. A staged tree is for review and packaging; it does not validate or grant trust to the runtime paths. Neither `--dry-run` nor staging loads the macOS daemon. To remove the installed macOS configuration, unload `org.vanken.chmod-bpf` with `launchctl bootout`, remove its plist and `chmod-bpf` script, and restore the BPF device ownership/permissions prescribed by your administrator.
42
+
3
43
  The application runs as your normal user. The capture helper opens the capture device, drops supplementary groups and both real and effective group/user IDs, and writes pcapng to stdout. The interpreter, helper, libraries, gems, and every parent directory of the privileged wrapper must be owned by root and must not be writable by group or other users.
4
44
 
5
45
  On macOS, grant the intended capture group access to `/dev/bpf*` using an administrator-managed startup configuration. Once `vanken-capture --check --interface lo0` reports `"direct":true`, Vanken can capture without sudo. Device permissions may need to be restored after a reboot.
@@ -30,4 +70,4 @@ Validate the rule with `visudo`. Members of this group can capture network traff
30
70
  sudo /usr/local/libexec/vanken/vanken-capture -i eth0 --drop-to "$(id -u):$(id -g)" > capture.pcapng
31
71
  ```
32
72
 
33
- For the isolated Linux integration check, run `script/capture-ci.sh` with Docker available. It creates a disposable privileged container, installs root-owned Ruby/gems and the fixed helper wrapper, creates the `vanken-test` namespace and a veth pair, then runs the tests as an unprivileged user. The tests verify UID dropping, packet capture, statistics, and clean shutdown. The nightly `Live capture` workflow runs this same command.
73
+ For the isolated Linux integration check, run `script/capture-ci.sh` with Docker available. It creates a disposable privileged container, runs `vanken-setup-permissions` against its root-owned Ruby/gems to install the fixed wrapper and sudoers policy, creates the `vanken-test` namespace and a veth pair, then runs the tests as an unprivileged user. The tests verify environment isolation, UID dropping, packet capture, statistics, and clean shutdown. The installer plan is saved as `permission-install.json`. The nightly `Live capture` workflow runs this same command.
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
3
+ <policyconfig>
4
+ <action id="org.vanken.capture">
5
+ <description>Capture network packets with Vanken</description>
6
+ <message>Authentication is required to capture network packets.</message>
7
+ <defaults>
8
+ <allow_any>no</allow_any>
9
+ <allow_inactive>no</allow_inactive>
10
+ <allow_active>auth_admin_keep</allow_active>
11
+ </defaults>
12
+ <annotate key="org.freedesktop.policykit.exec.path">/usr/local/libexec/vanken/vanken-capture</annotate>
13
+ </action>
14
+ </policyconfig>
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ cd / || exit 1
3
+ exec /usr/bin/env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin \
4
+ GEM_HOME=VANKEN_GEM_HOME GEM_PATH=VANKEN_GEM_HOME \
5
+ VANKEN_RUBY -I /usr/local/libexec/vanken/lib \
6
+ /usr/local/libexec/vanken/helper "$@"
@@ -0,0 +1,9 @@
1
+ [Desktop Entry]
2
+ Type=Application
3
+ Name=Vanken
4
+ Comment=Capture and inspect network packets
5
+ Exec=vanken %f
6
+ Icon=network-workgroup
7
+ Terminal=false
8
+ Categories=Network;Development;
9
+ MimeType=application/vnd.tcpdump.pcap;application/x-pcapng;
@@ -0,0 +1,2 @@
1
+ # Members of this group can inspect network traffic. Validate with visudo -cf.
2
+ %VANKEN_GROUP ALL=(root) NOPASSWD: /usr/local/libexec/vanken/vanken-capture
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ for device in /dev/bpf[0-9]*; do
3
+ [ -c "$device" ] || continue
4
+ /usr/bin/chgrp VANKEN_GROUP "$device" || exit 1
5
+ /bin/chmod 660 "$device" || exit 1
6
+ done
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0"><dict>
4
+ <key>Label</key><string>org.vanken.chmod-bpf</string>
5
+ <key>ProgramArguments</key><array><string>/usr/local/libexec/vanken/chmod-bpf</string></array>
6
+ <key>RunAtLoad</key><true/>
7
+ <key>StartInterval</key><integer>10</integer>
8
+ </dict></plist>
@@ -0,0 +1,43 @@
1
+ # Generated from lib/vanken/capture/permission_setup.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module Capture
5
+ class PermissionSetup
6
+ PREFIX: ::String
7
+
8
+ SOURCE: untyped
9
+
10
+ def self.run: (untyped argv, ?stdout: untyped, ?stderr: untyped) -> untyped
11
+
12
+ attr_reader platform: untyped
13
+
14
+ def initialize: (?platform: untyped, ?ruby: untyped, ?gem_home: untyped, ?policy: untyped, ?group: untyped, ?root: untyped) -> untyped
15
+
16
+ def staged?: () -> untyped
17
+
18
+ def plan: () -> untyped
19
+
20
+ def install: () -> untyped
21
+
22
+ def validate_runtime!: () -> untyped
23
+
24
+ def validate_group!: () -> untyped
25
+
26
+ def next_steps: () -> untyped
27
+
28
+ private
29
+
30
+ def asset: (untyped relative, ?mode: untyped) -> untyped
31
+
32
+ def trusted_tree!: (untyped path, ?untyped visited) -> untyped
33
+
34
+ def trusted_path!: (untyped path) -> untyped
35
+
36
+ def safe_entry!: (untyped path) -> untyped
37
+
38
+ def secure_directory!: (untyped path) -> untyped
39
+
40
+ def validate_sudoers!: (untyped path) -> untyped
41
+ end
42
+ end
43
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vanken
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -58,6 +58,7 @@ email:
58
58
  executables:
59
59
  - vanken
60
60
  - vanken-capture
61
+ - vanken-setup-permissions
61
62
  extensions: []
62
63
  extra_rdoc_files: []
63
64
  files:
@@ -72,6 +73,7 @@ files:
72
73
  - docs/usage.md
73
74
  - exe/vanken
74
75
  - exe/vanken-capture
76
+ - exe/vanken-setup-permissions
75
77
  - lib/vanken.rb
76
78
  - lib/vanken/app/capture_controller.rb
77
79
  - lib/vanken/app/document.rb
@@ -91,6 +93,7 @@ files:
91
93
  - lib/vanken/capture/helper_main.rb
92
94
  - lib/vanken/capture/helper_options.rb
93
95
  - lib/vanken/capture/launcher.rb
96
+ - lib/vanken/capture/permission_setup.rb
94
97
  - lib/vanken/capture/privileges.rb
95
98
  - lib/vanken/capture/receiver.rb
96
99
  - lib/vanken/cli.rb
@@ -142,6 +145,12 @@ files:
142
145
  - lib/vanken/ui/settings_operations.rb
143
146
  - lib/vanken/version.rb
144
147
  - packaging/README.md
148
+ - packaging/linux/org.vanken.capture.policy
149
+ - packaging/linux/vanken-capture.wrapper
150
+ - packaging/linux/vanken.desktop
151
+ - packaging/linux/vanken.sudoers
152
+ - packaging/macos/chmod-bpf
153
+ - packaging/macos/org.vanken.chmod-bpf.plist
145
154
  - sig/generated/vanken/app/capture_controller.rbs
146
155
  - sig/generated/vanken/app/document.rbs
147
156
  - sig/generated/vanken/app/document_analysis.rbs
@@ -160,6 +169,7 @@ files:
160
169
  - sig/generated/vanken/capture/helper_main.rbs
161
170
  - sig/generated/vanken/capture/helper_options.rbs
162
171
  - sig/generated/vanken/capture/launcher.rbs
172
+ - sig/generated/vanken/capture/permission_setup.rbs
163
173
  - sig/generated/vanken/capture/privileges.rbs
164
174
  - sig/generated/vanken/capture/receiver.rbs
165
175
  - sig/generated/vanken/cli.rbs