rbbcc 0.11.4 → 0.11.5

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: e4f674b42de394618b5c86e8b57a5d0edbf3132310eb11547f789ce098baa0f2
4
- data.tar.gz: a0d5a695da3347a3ad3752c650436f8f96386db7231c724b5f2af428383d60e9
3
+ metadata.gz: 4757d8938ba54c8aaab6d26a1d8cf824c325b8591130d1cf65ee13c0e411e99f
4
+ data.tar.gz: e41edc20c58336f5aa3e1670f2f001a9cdc829291a8bd63ab53d309790b81f5c
5
5
  SHA512:
6
- metadata.gz: '069947eb933d84024ba9b951fb8ca08768947bf13ce88b40b3d3dba1f99adc66dcb7e1a5c45deb7cddc4d28e476eddb877454e4e61b15a8ff1f85b2299bc0657'
7
- data.tar.gz: 0bba44088c66da0893a64e34e238e94c951c676838604ba43ad019f37bdcbae9b4eb586173e11e8f1919fd691334349155c9325ff98e634282afcb7c712c2ca4
6
+ metadata.gz: 43cb1ba2fa891fc6fd970b5a48b45c28d9fff967e25ef02ed5cb7b09ddc96a91dc0e4740dbc59b9bc78f312613b76f6b7f68de9ee41bf860caddf1fb0a948c15
7
+ data.tar.gz: e6e8321874553ba7df4f0529477d6bea3a35f4956b704bd32bfeb409d7f43a1a59f4f9484619891e1351ed278eee42c4edda7a17d176c3b911e4c5a1dee5716a
data/.gitignore CHANGED
@@ -7,6 +7,8 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /.agent
11
+ /.claude
10
12
 
11
13
  .ruby-version
12
14
 
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gem "bundler", "~> 2.0"
7
7
  gem "rake", "~> 13.0"
8
8
  gem "pry", "~> 0.12"
9
9
  gem "minitest", "~> 5"
10
+ gem "http-2", "~> 1.1"
10
11
 
11
12
  #group :omnibus_package do
12
13
  # gem "appbundler"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbbcc (0.11.4)
4
+ rbbcc (0.11.5)
5
5
  fiddle
6
6
 
7
7
  GEM
@@ -9,6 +9,7 @@ GEM
9
9
  specs:
10
10
  coderay (1.1.3)
11
11
  fiddle (1.1.8)
12
+ http-2 (1.1.3)
12
13
  io-console (0.8.2)
13
14
  method_source (1.1.0)
14
15
  minitest (5.27.0)
@@ -28,6 +29,7 @@ PLATFORMS
28
29
 
29
30
  DEPENDENCIES
30
31
  bundler (~> 2.0)
32
+ http-2 (~> 1.1)
31
33
  minitest (~> 5)
32
34
  pry (~> 0.12)
33
35
  rake (~> 13.0)
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rbbcc"
5
+ include RbBCC
6
+
7
+ begin
8
+ require "http/2"
9
+ rescue LoadError
10
+ # Fallback for local vendor source
11
+ local_http2_lib = File.expand_path("../.agent/sources/http-2/lib", __dir__)
12
+ $LOAD_PATH.unshift(local_http2_lib) unless $LOAD_PATH.include?(local_http2_lib)
13
+ require "http/2"
14
+ end
15
+
16
+ FRAME_TYPE_NAMES = {
17
+ 0x0 => "DATA",
18
+ 0x1 => "HEADERS",
19
+ 0x2 => "PRIORITY",
20
+ 0x3 => "RST_STREAM",
21
+ 0x4 => "SETTINGS",
22
+ 0x5 => "PUSH_PROMISE",
23
+ 0x6 => "PING",
24
+ 0x7 => "GOAWAY",
25
+ 0x8 => "WINDOW_UPDATE",
26
+ 0x9 => "CONTINUATION"
27
+ }.freeze
28
+
29
+ HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".b
30
+ HTTP1_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS TRACE CONNECT].freeze
31
+
32
+ BPF_TEXT = <<~BPF
33
+ #include <uapi/linux/ptrace.h>
34
+
35
+ #define MAX_PAYLOAD_SIZE 1024
36
+
37
+ struct event_t {
38
+ u32 pid;
39
+ char event_name[8];
40
+ unsigned char payload[MAX_PAYLOAD_SIZE];
41
+ u32 data_len;
42
+ s32 read_ret;
43
+ };
44
+
45
+ BPF_RINGBUF_OUTPUT(events, 256);
46
+
47
+ int trace_ssl_write(struct pt_regs *ctx) {
48
+ u32 tgid = bpf_get_current_pid_tgid() >> 32;
49
+
50
+ const char *user_buf = (const char *)PT_REGS_PARM2(ctx);
51
+ u64 num = (u64)PT_REGS_PARM3(ctx);
52
+ if (!user_buf || num <= 0) return 0;
53
+
54
+ struct event_t *event = events.ringbuf_reserve(sizeof(struct event_t));
55
+ if (!event) return 0;
56
+
57
+ event->pid = tgid;
58
+ __builtin_memcpy(event->event_name, "ssl_w", 6);
59
+
60
+ u32 len = num > MAX_PAYLOAD_SIZE ? MAX_PAYLOAD_SIZE : (u32)num;
61
+ event->data_len = len;
62
+
63
+ event->read_ret = bpf_probe_read_user(event->payload, len, user_buf);
64
+ if (event->read_ret < 0) {
65
+ event->data_len = 0;
66
+ }
67
+
68
+ events.ringbuf_submit(event, 0);
69
+ return 0;
70
+ }
71
+ BPF
72
+
73
+ def hex_ascii_dump(payload, width: 16)
74
+ lines = []
75
+ payload.bytes.each_slice(width).with_index do |slice, idx|
76
+ offset = idx * width
77
+ hex = slice.map { |b| "%02x" % b }.join(" ")
78
+ ascii = slice.map { |b| b >= 32 && b <= 126 ? b.chr : "." }.join
79
+ lines << format("%04x %-#{width * 3 - 1}s %s", offset, hex, ascii)
80
+ end
81
+ lines.join("\n")
82
+ end
83
+
84
+ def payload_from_event(event)
85
+ raw = event.payload
86
+ bytes = case raw
87
+ when String
88
+ raw.b
89
+ when Array
90
+ raw.map { |v| v & 0xff }.pack("C*")
91
+ else
92
+ if raw.respond_to?(:to_a)
93
+ raw.to_a.map { |v| v & 0xff }.pack("C*")
94
+ else
95
+ raw.to_s.b
96
+ end
97
+ end
98
+
99
+ len = [event.data_len.to_i, bytes.bytesize].min
100
+ bytes.byteslice(0, len) || "".b
101
+ end
102
+
103
+ def parse_http2_frames(payload)
104
+ frames = []
105
+ i = 0
106
+ total = payload.bytesize
107
+
108
+ while i + 9 <= total
109
+ length_bytes = payload.byteslice(i, 3).bytes
110
+ length = (length_bytes[0] << 16) | (length_bytes[1] << 8) | length_bytes[2]
111
+ frame_type = payload.getbyte(i + 3)
112
+ flags = payload.getbyte(i + 4)
113
+ stream_id = payload.byteslice(i + 5, 4).unpack1("N") & 0x7fff_ffff
114
+ i += 9
115
+ break if i + length > total
116
+
117
+ frame_payload = payload.byteslice(i, length)
118
+ i += length
119
+ frames << [frame_type, flags, stream_id, frame_payload]
120
+ end
121
+
122
+ frames
123
+ end
124
+
125
+ def extract_headers_fragment(frame_type, flags, frame_payload)
126
+ return frame_payload if frame_type == 0x9 # CONTINUATION
127
+ return nil unless frame_type == 0x1 # HEADERS only
128
+
129
+ start_idx = 0
130
+ end_idx = frame_payload.bytesize
131
+
132
+ if (flags & 0x08) != 0 # PADDED
133
+ return nil if end_idx.zero?
134
+
135
+ pad_len = frame_payload.getbyte(0)
136
+ start_idx += 1
137
+ end_idx = [start_idx, end_idx - pad_len].max
138
+ end
139
+
140
+ if (flags & 0x20) != 0 # PRIORITY
141
+ return nil if start_idx + 5 > end_idx
142
+
143
+ start_idx += 5
144
+ end
145
+
146
+ frame_payload.byteslice(start_idx, end_idx - start_idx) || "".b
147
+ end
148
+
149
+ def decode_pseudo_headers_with_hpack(header_block, decompressor)
150
+ pairs = decompressor.decode(header_block.b)
151
+ pseudo = {}
152
+ pairs.each do |k, v|
153
+ pseudo[k] = v
154
+ end
155
+ pseudo
156
+ rescue StandardError => e
157
+ { ":error" => e.message }
158
+ end
159
+
160
+ def maybe_http1_summary(payload)
161
+ text = payload.force_encoding(Encoding::UTF_8)
162
+ first_line = text.split("\r\n", 2).first
163
+ return nil if first_line.nil? || first_line.empty?
164
+
165
+ if HTTP1_METHODS.any? { |m| first_line.start_with?("#{m} ") }
166
+ return ["request", first_line]
167
+ end
168
+
169
+ if first_line.start_with?("HTTP/1.1 ") || first_line.start_with?("HTTP/1.0 ")
170
+ return ["response", first_line]
171
+ end
172
+
173
+ nil
174
+ rescue ArgumentError, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
175
+ nil
176
+ end
177
+
178
+ def main
179
+ puts "SSL HTTP tracer (Ruby + ring buffer) を初期化中..."
180
+
181
+ b = BCC.new(text: BPF_TEXT)
182
+
183
+ libssl_path = ENV.fetch("LIBSSL_PATH", "/usr/lib/aarch64-linux-gnu/libssl.so.3")
184
+ b.attach_uprobe(name: libssl_path, sym: "SSL_write", fn_name: "trace_ssl_write")
185
+ puts "Attached uprobe: SSL_write (#{libssl_path})"
186
+
187
+ decompressor = HTTP2::Header::Decompressor.new
188
+ continuation_state = {}
189
+
190
+ b["events"].open_ring_buffer do |_ctx, data, _size|
191
+ event = b["events"].event(data)
192
+ event_name = event.event_name.to_s
193
+ next unless event_name == "ssl_w"
194
+
195
+ payload = payload_from_event(event)
196
+ puts "[PID: #{event.pid}] len=#{event.data_len} read_ret=#{event.read_ret}"
197
+ next if event.data_len.to_i <= 0
198
+
199
+ summary = maybe_http1_summary(payload)
200
+ if summary
201
+ kind, line = summary
202
+ puts "[HTTP/1.x #{kind}] #{line}"
203
+ puts hex_ascii_dump(payload)
204
+ next
205
+ end
206
+
207
+ rest = payload
208
+ if rest.start_with?(HTTP2_PREFACE)
209
+ puts "[H2] client preface detected"
210
+ rest = rest.byteslice(HTTP2_PREFACE.bytesize..) || "".b
211
+ end
212
+
213
+ frames = parse_http2_frames(rest)
214
+ if frames.empty?
215
+ puts hex_ascii_dump(payload)
216
+ next
217
+ end
218
+
219
+ frames.each do |frame_type, flags, stream_id, frame_payload|
220
+ frame_name = FRAME_TYPE_NAMES.fetch(frame_type, "UNKNOWN(#{frame_type})")
221
+ puts "[H2] type=#{frame_name} flags=0x#{format('%02x', flags)} stream=#{stream_id} len=#{frame_payload.bytesize}"
222
+
223
+ case frame_type
224
+ when 0x1 # HEADERS
225
+ fragment = extract_headers_fragment(frame_type, flags, frame_payload)
226
+ if fragment.nil?
227
+ puts "[HPACK] parse_error=invalid HEADERS payload"
228
+ next
229
+ end
230
+
231
+ key = [event.pid.to_i, stream_id]
232
+ if (flags & 0x04) != 0 # END_HEADERS
233
+ pseudo = decode_pseudo_headers_with_hpack(fragment, decompressor)
234
+ if pseudo.key?(":error")
235
+ puts "[HPACK] decode_error=#{pseudo[":error"]}"
236
+ else
237
+ puts "[HPACK] :method=#{pseudo.fetch(":method", "?")} :path=#{pseudo.fetch(":path", "?")}"
238
+ end
239
+ else
240
+ continuation_state[key] = fragment.dup
241
+ puts "[HPACK] collecting CONTINUATION fragments"
242
+ end
243
+ when 0x9 # CONTINUATION
244
+ key = [event.pid.to_i, stream_id]
245
+ fragment = extract_headers_fragment(frame_type, flags, frame_payload)
246
+ unless continuation_state.key?(key)
247
+ puts "[HPACK] note=orphan CONTINUATION frame"
248
+ next
249
+ end
250
+
251
+ continuation_state[key] << fragment
252
+ next if (flags & 0x04).zero?
253
+
254
+ header_block = continuation_state.delete(key)
255
+ pseudo = decode_pseudo_headers_with_hpack(header_block, decompressor)
256
+ if pseudo.key?(":error")
257
+ puts "[HPACK] decode_error=#{pseudo[":error"]}"
258
+ else
259
+ puts "[HPACK] :method=#{pseudo.fetch(":method", "?")} :path=#{pseudo.fetch(":path", "?")}"
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ puts "監視開始。Ctrl+C で終了"
266
+ loop do
267
+ b.ring_buffer_poll(100)
268
+ rescue Interrupt
269
+ puts "\n終了します。"
270
+ exit(0)
271
+ end
272
+ end
273
+
274
+ main if $PROGRAM_NAME == __FILE__
data/lib/rbbcc/bcc.rb CHANGED
@@ -607,7 +607,10 @@ module RbBCC
607
607
  end
608
608
 
609
609
  def _open_ring_buffer(map_fd, fn, ctx)
610
- buf = Clib.bpf_new_ringbuf(map_fd, fn, ctx)
610
+ # Avoid GC'ing function pointer
611
+ @_ring_buffer_fns ||= []
612
+ @_ring_buffer_fns << fn
613
+ buf = Clib.bpf_new_ringbuf(map_fd, @_ring_buffer_fns[-1], ctx)
611
614
  if !buf
612
615
  raise "Could not open ring buffer"
613
616
  end
data/lib/rbbcc/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RbBCC
2
- VERSION = "0.11.4"
2
+ VERSION = "0.11.5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbbcc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.4
4
+ version: 0.11.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -101,6 +101,7 @@ files:
101
101
  - examples/py-orig/sockblock.py
102
102
  - examples/ruby_usdt.rb
103
103
  - examples/sbrk_trace.rb
104
+ - examples/ssl_http_trace.rb
104
105
  - examples/syscalluname.rb
105
106
  - examples/table.rb
106
107
  - examples/tools/bashreadline.rb