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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +2 -0
- data/docs/performance.md +43 -0
- data/exe/vanken-setup-permissions +6 -0
- data/lib/vanken/capture/permission_setup.rb +201 -0
- data/lib/vanken/version.rb +1 -1
- data/packaging/README.md +41 -1
- data/packaging/linux/org.vanken.capture.policy +14 -0
- data/packaging/linux/vanken-capture.wrapper +6 -0
- data/packaging/linux/vanken.desktop +9 -0
- data/packaging/linux/vanken.sudoers +2 -0
- data/packaging/macos/chmod-bpf +6 -0
- data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
- data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9db141f2b218a50e8cb5d228b14b35d6ff728e3fc48fbd6abddf18e56f903284
|
|
4
|
+
data.tar.gz: 65a40d15a931d59b2e618aab1cfbc9b8fd97bea13d0593b8b1938d611ba5bad2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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
|
data/lib/vanken/version.rb
CHANGED
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,
|
|
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,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.
|
|
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
|