vivarium 0.1.1 → 0.1.2

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: 1bc495e3c2b8f9d99a2b1c84706b5e00913cc08831b3103b0460db0d4027c77b
4
- data.tar.gz: 1ee086243fdc153da5bac6a2bec8d0afe8e644ceac2fef2308b7723b89533338
3
+ metadata.gz: 2fb7901d1f200e910b88b75fd8ba54cbaa2d6cf2152c31e35b6f790e8b9150e5
4
+ data.tar.gz: 045c6ef47acb148605a7dd7cbbda5526d82d3548191135baedecaa5d09840f25
5
5
  SHA512:
6
- metadata.gz: a203dd1cbaeff9fe0f4bf464a01ff49f131a104ca9e1eb14b6c08dd4371f49acf31c493119eee70114cafacbc6aa8a42976a9e6c230b1d828fe442e738c57c54
7
- data.tar.gz: a31a64e20f2f7d0956626658b5d6f91e0cd5333f97854ad3388c41cc0d3b0b8d233850c3bdaaf60663c9152d6cf35e4034355e37c50fc913687ad05295dffaba
6
+ metadata.gz: 9042d44b860bde781d512e36367bfc178a74dd57c6ae6be49c5cea73ec4fbd8b621382ace5d0e1b18188e36bae41d3f5e7e273fd8e019c1ec88820f95653b255
7
+ data.tar.gz: 9c22285362cea0fe871af6c1f67a66eeb85556a52c5f6aabe93da2b20daa51cbf6528df9021ead0fbb2acdcb152bbee60a1a873c9d7a9b75b1edc8ea244c6016
data/README.md CHANGED
@@ -18,10 +18,13 @@ The goal is to visualize which Ruby method context triggered low-level events.
18
18
  Implemented in this repository:
19
19
 
20
20
  - BPF LSM hook on `file_open`
21
+ - BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
22
+ - BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
23
+ - BPF kprobe on `ip_local_out` (captures UDP/53 DNS QNAME raw bytes as `dns_req`)
21
24
  - Shared pinned maps on bpffs
22
25
  - `config_root_targets` (root PID -> 0/1)
23
26
  - `config_spawned_targets` (spawned TID -> 0/1)
24
- - `event_invoked` (array length 64 with `event_t` records)
27
+ - `event_invoked` (array length 1024 with `event_t` records)
25
28
  - `event_write_pos` (cursor for appending into `event_invoked`)
26
29
  - Ruby API `Vivarium.observe do ... end`
27
30
  - Registers current PID to `config_root_targets`
@@ -36,8 +39,8 @@ Implemented in this repository:
36
39
  ```c
37
40
  struct event_t {
38
41
  u32 pid;
39
- char event_name[8]; // "path_open"
40
- char payload[64]; // opened path (truncated)
42
+ char event_name[16];
43
+ char payload[256];
41
44
  };
42
45
  ```
43
46
 
@@ -81,6 +84,14 @@ Vivarium.observe do
81
84
  end
82
85
  ```
83
86
 
87
+ 3) Network monitoring demo client:
88
+
89
+ ```bash
90
+ bundle exec ruby examples/network_client_demo.rb
91
+ ```
92
+
93
+ This demo intentionally triggers `sock_connect`, `dns_req`, and `odd_socket` events.
94
+
84
95
  You can also start top-level observation without a block (it keeps observing until process exit):
85
96
 
86
97
  ```ruby
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "socket"
5
+ require "vivarium"
6
+
7
+ # Usage:
8
+ # 1) In another shell (root): sudo bundle exec vivariumd
9
+ # 2) Run this script: bundle exec ruby examples/network_client_demo.rb
10
+
11
+ def try_step(title)
12
+ puts "[client] #{title}"
13
+ yield
14
+ rescue StandardError => e
15
+ puts "[client] #{title} failed: #{e.class}: #{e.message}"
16
+ end
17
+
18
+ Vivarium.observe do
19
+ # Likely emits sock_connect and dns_req via resolver traffic.
20
+ try_step("system: DNS lookup") do
21
+ system("getent hosts example.com >/dev/null 2>&1 || true")
22
+ system("getent hosts unknown.example.com >/dev/null 2>&1 || true")
23
+ end
24
+
25
+ # Likely emits sock_connect through HTTPS connection attempts.
26
+ try_step("system: curl") do
27
+ system("curl -I https://example.com >/dev/null 2>&1 || true")
28
+ end
29
+
30
+ # ICMP example (may require CAP_NET_RAW / root depending on environment).
31
+ try_step("system: ping") do
32
+ system("ping -c 1 example.com >/dev/null 2>&1 || true")
33
+ end
34
+
35
+ # Explicit connect path.
36
+ try_step("Ruby TCP connect") do
37
+ sock = TCPSocket.new("example.com", 80)
38
+ sock.close
39
+ end
40
+
41
+ # Raw DNS query payload, useful for dns_req decode testing.
42
+ try_step("Ruby UDP DNS query") do
43
+ dns_query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" +
44
+ "\x09udp-query\x07example\x03com\x00" +
45
+ "\x00\x01\x00\x01"
46
+
47
+ udp = UDPSocket.new
48
+ begin
49
+ udp.connect("127.0.0.53", 53)
50
+ rescue StandardError
51
+ udp.connect("8.8.8.8", 53)
52
+ end
53
+ udp.send(dns_query, 0)
54
+ udp.close
55
+ end
56
+
57
+ # Explicit sendto path for DNS payload visibility.
58
+ try_step("Ruby UDP sendto DNS query") do
59
+ dns_query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" +
60
+ "\x06sendto\x07example\x03com\x00" +
61
+ "\x00\x01\x00\x01"
62
+
63
+ udp = UDPSocket.new
64
+ udp.send(dns_query, 0, "127.0.0.53", 53)
65
+ udp.close
66
+ end
67
+
68
+ # Intentionally unusual socket type to trigger odd_socket.
69
+ try_step("Ruby odd socket attempt") do
70
+ af_packet = Socket.const_defined?(:AF_PACKET) ? Socket::AF_PACKET : 17
71
+ raw = Socket.new(af_packet, Socket::SOCK_RAW, 0)
72
+ raw.close
73
+ end
74
+ end
75
+
76
+ puts "[client] done"
@@ -45,7 +45,7 @@ module Vivarium
45
45
  @io.puts "[vivarium] #{events.size} event(s) at #{tp.defined_class}##{tp.method_id} (#{tp.event})"
46
46
  @io.puts " location: #{tp.path}:#{tp.lineno}"
47
47
  events.each do |event|
48
- @io.puts " pid=#{event.pid} #{event.event_name} payload=#{event.payload.inspect}"
48
+ @io.puts " ktime_ns=#{event.ktime_ns} pid=#{event.pid} #{event.event_name} payload=#{Vivarium.render_event_payload(event)}"
49
49
  end
50
50
  @io.puts " stack:"
51
51
  stack.each do |loc|
@@ -59,7 +59,7 @@ module Vivarium
59
59
  event: tp.event.to_s,
60
60
  path: tp.path,
61
61
  lineno: tp.lineno,
62
- events: events.map { |e| { pid: e.pid, event_name: e.event_name, payload: e.payload } },
62
+ events: events.map { |e| { ktime_ns: e.ktime_ns, pid: e.pid, event_name: e.event_name, payload: Vivarium.render_event_payload(e) } },
63
63
  stack: stack.map { |loc| "#{loc.path}:#{loc.lineno}:in #{loc.base_label}" }
64
64
  }
65
65
  @io.puts JSON.generate(entry)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "optparse"
6
6
  require "pathname"
7
7
  require "rbbcc"
8
+ require "socket"
8
9
  require_relative "vivarium/version"
9
10
  require_relative "vivarium/logger"
10
11
 
@@ -20,8 +21,13 @@ module Vivarium
20
21
 
21
22
  EVENT_NAME_SIZE = 16
22
23
  EVENT_PAYLOAD_SIZE = 256
23
- EVENT_STRUCT_SIZE = 4 + EVENT_NAME_SIZE + EVENT_PAYLOAD_SIZE
24
- EVENT_CAPACITY = 64
24
+ EVENT_TS_SIZE = 8
25
+ EVENT_STRUCT_SIZE = 288
26
+ EVENT_TS_OFFSET = 0
27
+ EVENT_PID_OFFSET = 8
28
+ EVENT_NAME_OFFSET = 12
29
+ EVENT_PAYLOAD_OFFSET = 28
30
+ EVENT_CAPACITY = 1024
25
31
 
26
32
  @bpf_pin_dir = PIN_DIR
27
33
 
@@ -33,20 +39,26 @@ module Vivarium
33
39
  end
34
40
  end
35
41
 
36
- Event = Struct.new(:pid, :event_name, :payload, keyword_init: true) do
42
+ Event = Struct.new(:ktime_ns, :pid, :event_name, :payload, keyword_init: true) do
37
43
  def empty?
38
- pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
44
+ ktime_ns.to_i.zero? && pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
39
45
  end
40
46
 
41
47
  def self.from_binary(raw)
42
48
  bytes = raw.to_s.b
43
49
  bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00")
44
50
 
45
- pid = bytes[0, 4].unpack1("L<")
46
- event_name = c_string(bytes[4, EVENT_NAME_SIZE])
47
- payload = c_string(bytes[4 + EVENT_NAME_SIZE, EVENT_PAYLOAD_SIZE])
48
-
49
- new(pid: pid, event_name: event_name, payload: payload)
51
+ ktime_ns = bytes[EVENT_TS_OFFSET, EVENT_TS_SIZE].unpack1("Q<")
52
+ pid = bytes[EVENT_PID_OFFSET, 4].unpack1("L<")
53
+ event_name = c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE])
54
+ raw_payload = bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE]
55
+ payload = if %w[dns_req sock_connect odd_socket].include?(event_name)
56
+ raw_payload
57
+ else
58
+ c_string(raw_payload)
59
+ end
60
+
61
+ new(ktime_ns: ktime_ns, pid: pid, event_name: event_name, payload: payload)
50
62
  end
51
63
 
52
64
  def self.c_string(bytes)
@@ -58,6 +70,95 @@ module Vivarium
58
70
  end
59
71
  end
60
72
 
73
+ def self.decode_dns_qname(raw_payload)
74
+ bytes = raw_payload.to_s.b.bytes
75
+ labels = []
76
+ idx = 0
77
+
78
+ while idx < bytes.length
79
+ length = bytes[idx]
80
+ break if length.nil? || length.zero?
81
+ break if length > 63
82
+
83
+ idx += 1
84
+ break if (idx + length) > bytes.length
85
+
86
+ label = bytes[idx, length].pack("C*")
87
+ labels << label
88
+ idx += length
89
+ end
90
+
91
+ return "" if labels.empty?
92
+
93
+ labels.join(".")
94
+ end
95
+
96
+ def self.decode_sock_connect_payload(raw_payload)
97
+ bytes = raw_payload.to_s.b
98
+ return "" if bytes.bytesize < 20
99
+
100
+ family = bytes[0, 2].unpack1("S<")
101
+ port = bytes[2, 2].unpack1("n")
102
+ addr = bytes[4, 16]
103
+
104
+ case family
105
+ when 2 # AF_INET
106
+ ipv4 = addr[0, 4].bytes.join(".")
107
+ "#{ipv4}:#{port} (#{socket_const_name("AF_", family)})"
108
+ when 10 # AF_INET6
109
+ words = addr.unpack("n8")
110
+ ipv6 = words.map { |w| format("%x", w) }.join(":")
111
+ "[#{ipv6}]:#{port} (#{socket_const_name("AF_", family)})"
112
+ else
113
+ "family=#{family}(#{socket_const_name("AF_", family)}) port=#{port}"
114
+ end
115
+ end
116
+
117
+ def self.decode_odd_socket_payload(raw_payload)
118
+ bytes = raw_payload.to_s.b
119
+ return "" if bytes.bytesize < 6
120
+
121
+ family = bytes[0, 2].unpack1("S<")
122
+ type = bytes[2, 2].unpack1("S<")
123
+ protocol = bytes[4, 2].unpack1("S<")
124
+ family_name = socket_const_name("AF_", family)
125
+ type_name = socket_const_name("SOCK_", type)
126
+ protocol_name = socket_const_name("IPPROTO_", protocol)
127
+ "family=#{family}(#{family_name}) type=#{type}(#{type_name}) protocol=#{protocol}(#{protocol_name})"
128
+ end
129
+
130
+ def self.socket_const_name(prefix, value)
131
+ return "UNKNOWN" unless defined?(Socket)
132
+
133
+ key = Socket.constants.find do |name|
134
+ name.to_s.start_with?(prefix) && Socket.const_get(name) == value
135
+ rescue NameError
136
+ false
137
+ end
138
+
139
+ key ? key.to_s : "UNKNOWN"
140
+ end
141
+
142
+ def self.decode_bad_socket_payload(raw_payload)
143
+ decode_odd_socket_payload(raw_payload)
144
+ end
145
+
146
+ def self.render_event_payload(event)
147
+ case event.event_name
148
+ when "dns_req"
149
+ decoded = decode_dns_qname(event.payload)
150
+ decoded.empty? ? event.payload.inspect : decoded
151
+ when "sock_connect"
152
+ decoded = decode_sock_connect_payload(event.payload)
153
+ decoded.empty? ? event.payload.inspect : decoded
154
+ when "odd_socket"
155
+ decoded = decode_odd_socket_payload(event.payload)
156
+ decoded.empty? ? event.payload.inspect : decoded
157
+ else
158
+ event.payload.inspect
159
+ end
160
+ end
161
+
61
162
  class MapStore
62
163
  def initialize(pin_dir: Vivarium.bpf_pin_dir)
63
164
  @pin_dir = pin_dir
@@ -132,6 +233,23 @@ module Vivarium
132
233
 
133
234
  class Daemon
134
235
  BPF_PROGRAM_TEMPLATE = <<~CLANG
236
+ #include <linux/socket.h>
237
+ #include <uapi/linux/in.h>
238
+ #include <uapi/linux/in6.h>
239
+ #include <uapi/linux/ip.h>
240
+ #include <uapi/linux/udp.h>
241
+
242
+ #ifndef SOCK_STREAM
243
+ #define SOCK_STREAM 1
244
+ #endif
245
+ #ifndef SOCK_DGRAM
246
+ #define SOCK_DGRAM 2
247
+ #endif
248
+
249
+ struct net;
250
+ struct sock;
251
+ struct sk_buff;
252
+
135
253
  struct path {
136
254
  void *mnt;
137
255
  void *dentry;
@@ -141,7 +259,62 @@ module Vivarium
141
259
  struct path f_path;
142
260
  };
143
261
 
262
+ struct sockaddr_t {
263
+ u16 sa_family;
264
+ unsigned char sa_data[14];
265
+ };
266
+
267
+ struct sockaddr_in_t {
268
+ u16 sin_family;
269
+ u16 sin_port;
270
+ u32 sin_addr;
271
+ unsigned char pad[8];
272
+ };
273
+
274
+ struct sockaddr_in6_t {
275
+ u16 sin6_family;
276
+ u16 sin6_port;
277
+ u32 sin6_flowinfo;
278
+ unsigned char sin6_addr[16];
279
+ u32 sin6_scope_id;
280
+ };
281
+
282
+ struct sockaddr_port_t {
283
+ u16 family;
284
+ u16 port;
285
+ };
286
+
287
+ struct iovec_t {
288
+ void *iov_base;
289
+ unsigned long iov_len;
290
+ };
291
+
292
+ struct user_msghdr_t {
293
+ void *msg_name;
294
+ int msg_namelen;
295
+ struct iovec_t *msg_iov;
296
+ unsigned long msg_iovlen;
297
+ void *msg_control;
298
+ unsigned long msg_controllen;
299
+ unsigned int msg_flags;
300
+ };
301
+
302
+ struct mmsghdr_t {
303
+ struct user_msghdr_t msg_hdr;
304
+ unsigned int msg_len;
305
+ };
306
+
307
+ struct sk_buff_t {
308
+ unsigned char *head;
309
+ unsigned char *data;
310
+ u32 len;
311
+ u16 mac_header;
312
+ u16 network_header;
313
+ u16 transport_header;
314
+ };
315
+
144
316
  struct event_t {
317
+ u64 ktime_ns;
145
318
  u32 pid;
146
319
  char event_name[16];
147
320
  char payload[#{EVENT_PAYLOAD_SIZE}];
@@ -149,7 +322,8 @@ module Vivarium
149
322
 
150
323
  BPF_HASH(config_root_targets, u32, u8, 1024);
151
324
  BPF_HASH(config_spawned_targets, u32, u8, 8192);
152
- BPF_ARRAY(event_invoked, struct event_t, 1024);
325
+ BPF_HASH(dns_connected_tids, u32, u8, 8192);
326
+ BPF_ARRAY(event_invoked, struct event_t, #{EVENT_CAPACITY});
153
327
  BPF_ARRAY(event_write_pos, u32, 1);
154
328
 
155
329
  static __always_inline int target_enabled(u32 pid, u32 tid)
@@ -167,6 +341,61 @@ module Vivarium
167
341
  return 0;
168
342
  }
169
343
 
344
+ static __always_inline void submit_event(struct event_t *ev)
345
+ {
346
+ u32 zero = 0;
347
+ u32 *write_pos = event_write_pos.lookup(&zero);
348
+ if (!write_pos) {
349
+ return;
350
+ }
351
+
352
+ ev->ktime_ns = bpf_ktime_get_ns();
353
+
354
+ u32 idx = *write_pos % #{EVENT_CAPACITY};
355
+ __sync_fetch_and_add(write_pos, 1);
356
+ event_invoked.update(&idx, ev);
357
+ }
358
+
359
+ static __always_inline int is_dns_destination(void *addr)
360
+ {
361
+ u16 family = 0;
362
+ bpf_probe_read_user(&family, sizeof(family), addr);
363
+
364
+ if (family == AF_INET) {
365
+ struct sockaddr_in_t sin = {};
366
+ bpf_probe_read_user(&sin, sizeof(sin), addr);
367
+ return sin.sin_port == __constant_htons(53);
368
+ }
369
+
370
+ if (family == AF_INET6) {
371
+ struct sockaddr_in6_t sin6 = {};
372
+ bpf_probe_read_user(&sin6, sizeof(sin6), addr);
373
+ return sin6.sin6_port == __constant_htons(53);
374
+ }
375
+
376
+ return 0;
377
+ }
378
+
379
+ static __always_inline void submit_dns_req(u32 pid, unsigned char *payload, unsigned int payload_len)
380
+ {
381
+ unsigned int copy_len = payload_len;
382
+
383
+ if (copy_len <= 12) {
384
+ return;
385
+ }
386
+
387
+ copy_len -= 12;
388
+ if (copy_len > 64) {
389
+ copy_len = 64;
390
+ }
391
+
392
+ struct event_t ev = {};
393
+ ev.pid = pid;
394
+ __builtin_memcpy(ev.event_name, "dns_req", 8);
395
+ bpf_probe_read_user(&ev.payload[0], copy_len, payload + 12);
396
+ submit_event(&ev);
397
+ }
398
+
170
399
  TRACEPOINT_PROBE(sched, sched_process_fork)
171
400
  {
172
401
  u32 parent = args->parent_pid;
@@ -191,6 +420,7 @@ module Vivarium
191
420
  {
192
421
  u32 tid = (u32)bpf_get_current_pid_tgid();
193
422
  config_spawned_targets.delete(&tid);
423
+ dns_connected_tids.delete(&tid);
194
424
  return 0;
195
425
  }
196
426
 
@@ -204,14 +434,6 @@ module Vivarium
204
434
  return 0;
205
435
  }
206
436
 
207
- u32 zero = 0;
208
- u32 *write_pos = event_write_pos.lookup(&zero);
209
- if (!write_pos) {
210
- return 0;
211
- }
212
-
213
- u32 idx = *write_pos & 1023;
214
- __sync_fetch_and_add(write_pos, 1);
215
437
  struct event_t ev = {};
216
438
  int path_ret;
217
439
  ev.pid = pid;
@@ -226,7 +448,185 @@ module Vivarium
226
448
  }
227
449
 
228
450
  bpf_trace_printk("vivarium: pid=%d path=%s\\n", pid, ev.payload);
229
- event_invoked.update(&idx, &ev);
451
+ submit_event(&ev);
452
+
453
+ return 0;
454
+ }
455
+
456
+ LSM_PROBE(socket_create, int family, int type, int protocol, int kern)
457
+ {
458
+ u64 pid_tgid = bpf_get_current_pid_tgid();
459
+ u32 pid = pid_tgid >> 32;
460
+ u32 tid = (u32)pid_tgid;
461
+
462
+ if (!target_enabled(pid, tid)) {
463
+ return 0;
464
+ }
465
+
466
+ if ((family == AF_INET || family == AF_INET6) && (type == SOCK_STREAM || type == SOCK_DGRAM)) {
467
+ return 0;
468
+ }
469
+
470
+ struct event_t ev = {};
471
+ u16 family16 = family;
472
+ u16 type16 = type;
473
+ u16 proto16 = protocol;
474
+
475
+ ev.pid = pid;
476
+ __builtin_memcpy(ev.event_name, "odd_socket", 11);
477
+ __builtin_memcpy(&ev.payload[0], &family16, sizeof(family16));
478
+ __builtin_memcpy(&ev.payload[2], &type16, sizeof(type16));
479
+ __builtin_memcpy(&ev.payload[4], &proto16, sizeof(proto16));
480
+ submit_event(&ev);
481
+
482
+ return 0;
483
+ }
484
+
485
+ LSM_PROBE(socket_connect, struct socket *sock, struct sockaddr *address, int addrlen)
486
+ {
487
+ u64 pid_tgid = bpf_get_current_pid_tgid();
488
+ u32 pid = pid_tgid >> 32;
489
+ u32 tid = (u32)pid_tgid;
490
+ u16 family = 0;
491
+ u8 one = 1;
492
+
493
+ if (!target_enabled(pid, tid)) {
494
+ return 0;
495
+ }
496
+
497
+ if (!address) {
498
+ return 0;
499
+ }
500
+
501
+ bpf_probe_read_kernel(&family, sizeof(family), address);
502
+
503
+ struct event_t ev = {};
504
+ ev.pid = pid;
505
+ __builtin_memcpy(ev.event_name, "sock_connect", 13);
506
+ __builtin_memcpy(&ev.payload[0], &family, sizeof(family));
507
+
508
+ if (family == AF_INET) {
509
+ struct sockaddr_in_t sin = {};
510
+ bpf_probe_read_kernel(&sin, sizeof(sin), address);
511
+ __builtin_memcpy(&ev.payload[2], &sin.sin_port, sizeof(sin.sin_port));
512
+ __builtin_memcpy(&ev.payload[4], &sin.sin_addr, sizeof(sin.sin_addr));
513
+ if (sin.sin_port == __constant_htons(53)) {
514
+ dns_connected_tids.update(&tid, &one);
515
+ }
516
+ } else if (family == AF_INET6) {
517
+ struct sockaddr_in6_t sin6 = {};
518
+ bpf_probe_read_kernel(&sin6, sizeof(sin6), address);
519
+ __builtin_memcpy(&ev.payload[2], &sin6.sin6_port, sizeof(sin6.sin6_port));
520
+ __builtin_memcpy(&ev.payload[4], &sin6.sin6_addr, sizeof(sin6.sin6_addr));
521
+ if (sin6.sin6_port == __constant_htons(53)) {
522
+ dns_connected_tids.update(&tid, &one);
523
+ }
524
+ }
525
+
526
+ submit_event(&ev);
527
+
528
+ return 0;
529
+ }
530
+
531
+ TRACEPOINT_PROBE(syscalls, sys_enter_sendmsg)
532
+ {
533
+ u64 pid_tgid = bpf_get_current_pid_tgid();
534
+ u32 pid = pid_tgid >> 32;
535
+ u32 tid = (u32)pid_tgid;
536
+ struct user_msghdr_t msg = {};
537
+ struct iovec_t iov = {};
538
+
539
+ if (!target_enabled(pid, tid)) {
540
+ return 0;
541
+ }
542
+
543
+ if (!args->msg) {
544
+ return 0;
545
+ }
546
+
547
+ bpf_probe_read_user(&msg, sizeof(msg), args->msg);
548
+ if (!msg.msg_iov || msg.msg_iovlen == 0) {
549
+ return 0;
550
+ }
551
+
552
+ if (msg.msg_name && !is_dns_destination(msg.msg_name)) {
553
+ return 0;
554
+ }
555
+
556
+ bpf_probe_read_user(&iov, sizeof(iov), msg.msg_iov);
557
+ if (!iov.iov_base) {
558
+ return 0;
559
+ }
560
+
561
+ submit_dns_req(pid, (unsigned char *)iov.iov_base, (unsigned int)iov.iov_len);
562
+
563
+ return 0;
564
+ }
565
+
566
+ TRACEPOINT_PROBE(syscalls, sys_enter_sendto)
567
+ {
568
+ u64 pid_tgid = bpf_get_current_pid_tgid();
569
+ u32 pid = pid_tgid >> 32;
570
+ u32 tid = (u32)pid_tgid;
571
+ unsigned char *buff = args->buff;
572
+ int dns_match = 0;
573
+
574
+ if (!target_enabled(pid, tid)) {
575
+ return 0;
576
+ }
577
+
578
+ if (!buff) {
579
+ return 0;
580
+ }
581
+
582
+ if (args->addr) {
583
+ dns_match = is_dns_destination(args->addr);
584
+ } else {
585
+ u8 *connected = dns_connected_tids.lookup(&tid);
586
+ dns_match = connected && *connected == 1;
587
+ }
588
+
589
+ if (!dns_match) {
590
+ return 0;
591
+ }
592
+
593
+ submit_dns_req(pid, buff, args->len);
594
+ dns_connected_tids.delete(&tid);
595
+
596
+ return 0;
597
+ }
598
+
599
+ TRACEPOINT_PROBE(syscalls, sys_enter_sendmmsg)
600
+ {
601
+ u64 pid_tgid = bpf_get_current_pid_tgid();
602
+ u32 pid = pid_tgid >> 32;
603
+ u32 tid = (u32)pid_tgid;
604
+ struct mmsghdr_t mmsg = {};
605
+ struct iovec_t iov = {};
606
+
607
+ if (!target_enabled(pid, tid)) {
608
+ return 0;
609
+ }
610
+
611
+ if (!args->mmsg) {
612
+ return 0;
613
+ }
614
+
615
+ bpf_probe_read_user(&mmsg, sizeof(mmsg), args->mmsg);
616
+ if (mmsg.msg_hdr.msg_name && !is_dns_destination(mmsg.msg_hdr.msg_name)) {
617
+ return 0;
618
+ }
619
+
620
+ if (!mmsg.msg_hdr.msg_iov || mmsg.msg_hdr.msg_iovlen == 0) {
621
+ return 0;
622
+ }
623
+
624
+ bpf_probe_read_user(&iov, sizeof(iov), mmsg.msg_hdr.msg_iov);
625
+ if (!iov.iov_base) {
626
+ return 0;
627
+ }
628
+
629
+ submit_dns_req(pid, (unsigned char *)iov.iov_base, (unsigned int)iov.iov_len);
230
630
 
231
631
  return 0;
232
632
  }
data/sig/vivarium.rbs CHANGED
@@ -5,6 +5,7 @@ module Vivarium
5
5
  end
6
6
 
7
7
  class Event < ::Struct
8
+ attr_accessor ktime_ns: Integer?
8
9
  attr_accessor pid: Integer?
9
10
  attr_accessor event_name: String?
10
11
  attr_accessor payload: String?
@@ -39,11 +40,21 @@ module Vivarium
39
40
 
40
41
  EVENT_NAME_SIZE: Integer
41
42
  EVENT_PAYLOAD_SIZE: Integer
43
+ EVENT_TS_SIZE: Integer
42
44
  EVENT_STRUCT_SIZE: Integer
45
+ EVENT_TS_OFFSET: Integer
46
+ EVENT_PID_OFFSET: Integer
47
+ EVENT_NAME_OFFSET: Integer
48
+ EVENT_PAYLOAD_OFFSET: Integer
43
49
  EVENT_CAPACITY: Integer
44
50
 
45
51
  def self.bpf_pin_dir: () -> String
46
52
  def self.bpf_pin_dir=: (String dir) -> String
53
+ def self.decode_dns_qname: (String raw_payload) -> String
54
+ def self.decode_sock_connect_payload: (String raw_payload) -> String
55
+ def self.decode_odd_socket_payload: (String raw_payload) -> String
56
+ def self.decode_bad_socket_payload: (String raw_payload) -> String
57
+ def self.render_event_payload: (Event event) -> String
47
58
  def self.observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) { () -> untyped } -> untyped
48
59
  def self.top_observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) -> ObservationSession
49
60
  def self.filter_internal_frames?: () -> bool
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vivarium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.11.3
18
+ version: 0.11.4
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.11.3
25
+ version: 0.11.4
26
26
  description: Vivarium visualizes low-level events such as file open paths and relates
27
27
  them to Ruby method boundaries by combining RbBCC (eBPF LSM) and TracePoint.
28
28
  email:
@@ -34,6 +34,7 @@ extra_rdoc_files: []
34
34
  files:
35
35
  - README.md
36
36
  - Rakefile
37
+ - examples/network_client_demo.rb
37
38
  - exe/vivariumd
38
39
  - lib/vivarium.rb
39
40
  - lib/vivarium/logger.rb